2015-03-17 8 views
16

La seguente funzione riprende il testo selezionato in un controllo Richedit, scrive su un TMemoryStream all'interno di una funzione di callback e quindi restituisce come stringa di testo normale il codice rtf non elaborato.Perché questo codice ha esito negativo quando si dichiara TMemoryStream localmente ma funziona quando viene dichiarato globalmente?

var 
    MS: TMemoryStream; // declared globally and works. 

implementation 

function GetSelectedRTFCode(RichEdit: TRichedit): string; 

    function RichEditCallBack(dwCookie: Longint; pbBuff: PByte; 
    CB: Longint; var pCB: Pointer): Longint; stdcall; 
    begin 
    MS.WriteBuffer(pbBuff^, CB); 
    Result := CB; 
    end; 

var 
    EditStream: TEditStream; 
    SL: TStringList; 
begin 
    MS := TMemoryStream.Create; 
    try 
    EditStream.dwCookie  := SF_RTF or SFF_SELECTION; 
    EditStream.dwError  := 0; 
    EditStream.pfnCallback := @RichEditCallBack; 
    Richedit.Perform(EM_StreamOut, SF_RTF or SFF_SELECTION, DWord(@EditStream)); 
    MS.Seek(0, soBeginning); 

    SL := TStringList.Create; 
    try 
     SL.LoadFromStream(MS); 
     Result := SL.Text; 
    finally 
     SL.Free; 
    end; 
    finally 
    MS.Free; 
    end; 
end; 

Quanto sopra funziona come previsto senza errori.

Tuttavia, cerco di evitare le variabili dichiarate a livello globale, quando possibile e tenerli locale alla procedura o funzione che ne ha bisogno, ma per qualche motivo dichiarando MS: TMemoryStream; all'interno della funzione GetSelectedRTFCode fallisce con istruzioni Priviliged e gli errori di violazione di accesso.

Quindi, con questo in mente, e l'unico cambiamento in basso stato MS: TMemoryStream; dichiarato localmente fallisce:

function GetSelectedRTFCode(RichEdit: TRichedit): string; 
var 
    MS: TMemoryStream; // declare here instead of globally but fails. 

    function RichEditCallBack(dwCookie: Longint; pbBuff: PByte; 
    CB: Longint; var pCB: Pointer): Longint; stdcall; 
    begin 
    MS.WriteBuffer(pbBuff^, CB); 
    Result := CB; 
    end; 

var 
    EditStream: TEditStream; 
    SL: TStringList; 
begin 
    MS := TMemoryStream.Create; 
    try 
    EditStream.dwCookie  := SF_RTF or SFF_SELECTION; 
    EditStream.dwError  := 0; 
    EditStream.pfnCallback := @RichEditCallBack; 
    Richedit.Perform(EM_StreamOut, SF_RTF or SFF_SELECTION, DWord(@EditStream)); 
    MS.Seek(0, soBeginning); 

    SL := TStringList.Create; 
    try 
     SL.LoadFromStream(MS); 
     Result := SL.Text; 
    finally 
     SL.Free; 
    end; 
    finally 
    MS.Free; 
    end; 
end; 

Perchè dovrebbe dichiarare una variabile flusso di memoria di lavoro a livello globale, ma fallire quando dichiarato localmente?

risposta

20

Il problema è che l'uso di una funzione nidificata come callback è errato. Per una possibilità di implementazione usando una funzione nidificata in questo modo funziona per il compilatore a 32 bit, purché la funzione annidata non si riferisca a nessuna variabile locale delle funzioni circostanti.

Tuttavia, non appena la funzione nidificata fa riferimento a tali variabili locali, è necessario passare un parametro nascosto aggiuntivo in modo che la funzione nidificata possa accedere ai frame di stack delle funzioni circostanti. E per il compilatore a 64 bit, viene sempre passato un parametro extra nascosto.

Troverete molti esempi sul web dove le persone dimostrano di passare funzioni annidate come callback. Ma tutti questi esempi rompere le documented regole della lingua:

procedure nidificate e funzioni (routine dichiarate all'interno altre routine) non possono essere utilizzati come valori procedurali, né si può predefinite procedure e funzioni.

Quello che dovete fare è smettere di usare funzioni annidate per i callback. È necessario dichiarare la funzione di callback come dotata di ambito globale. Passare il flusso di memoria tramite il membro dwCookie della struttura EDITSTREAM.

// This compiles now, but the callback implementation is wrong, see below 

function RichEditCallBack(dwCookie: DWORD_PTR; pbBuff: PByte; 
    CB: Longint; var pCB: Longint): Longint; stdcall; 
var 
    MS: TMemoryStream; 
begin 
    MS := TMemoryStream(dwCookie); 
    MS.WriteBuffer(pbBuff^, CB); 
    Result := CB; 
end; 

function GetSelectedRTFCode(RichEdit: TRichedit): string; 
var 
    MS: TMemoryStream; 
    EditStream: TEditStream; 
    SL: TStringList; 
begin 
    MS := TMemoryStream.Create; 
    try 
    EditStream.dwCookie  := DWORD_PTR(MS); 
    EditStream.dwError  := 0; 
    EditStream.pfnCallback := RichEditCallBack; 
    Richedit.Perform(EM_StreamOut, SF_RTF or SFF_SELECTION, LPARAM(@EditStream)); 
    MS.Seek(0, soBeginning); 

    SL := TStringList.Create; 
    try 
     SL.LoadFromStream(MS); 
     Result := SL.Text; 
    finally 
     SL.Free; 
    end; 
    finally 
    MS.Free; 
    end; 
end; 

nota in particolare che non ho usato l'operatore @ per ottenere l'indirizzo della funzione di callback. L'utilizzo dell'operatore @ su una funzione determina una soppressione del controllo del tipo. Se non avessi usato l'operatore @, il compilatore sarebbe stato in grado di dirti i tuoi errori.

Il compilatore avrebbero detto:

 
[dcc32 Error] E2094 Local procedure/function 'RichEditCallBack' assigned to 
procedure variable 

E nota anche che il codice dichiara il tipo di parametro finale in modo non corretto. È un parametro di riferimento di tipo Longint. Ancora una volta, il compilatore può segnalarlo e lo segnala, a meno che non si sia utilizzato @ per ottenere l'indirizzo della funzione.

Questo secondo errore porta all'implementazione del callback. Non è corretto Il valore restituito indica successo.Un valore pari a zero viene utilizzato per indicare il successo, qualsiasi altro valore indica un errore. Il numero di byte scritti deve essere restituito tramite il parametro finale. Il tuo callback dovrebbe apparire così:

function RichEditCallBack(dwCookie: DWORD_PTR; pbBuff: PByte; 
    CB: Longint; var CBWritten: Longint): Longint; stdcall; 
var 
    MS: TMemoryStream; 
begin 
    MS := TMemoryStream(dwCookie); 
    CBWritten := MS.Write(pbBuff^, CB); 
    Result := IfThen(CB = CBWritten, 0, 1); 
end; 
+0

È solo System.Math necessario per IfThen. Non l'ho menzionato perché sospettavo che tu sapessi dove trovarlo. Può essere fatto anche con una dichiarazione if. –

+0

Puoi anche ['aggiungere un po 'di zucchero'] (http://pastebin.com/udGkgwUz). Btw. sembra che EMBT si stia avvicinando al prototipo corretto di quella richiamata (non ancora corretta, ma più vicina). È cambiato da Delphi XE3. Forse in XE8 sarà finalmente corretto. – TLama

+0

@TLama Ah, le gioie dei prototipi Win32 tradotti male. In questo caso non importa tanto perché è 0, o non 0. –

Problemi correlati