Chi lavora con Delphi probabilmente sa che il matrimonio tra Delphi e InterBase/Firebird è un matrimonio d’amore. Se avete avuto modo di apprezzare la flessibilità del componente TIBDataSet, che consente di definire statement separati per le operazioni di SELECT, INSERT, UPDATE e DELETE, in contrapposizione all’opacità dei componenti ADO, che celano gran parte della complessità a vantaggio della facilità d’uso ma a discapito di una maggior granularità di controllo da parte del programmatore, fiuuuu che frase, se avete avuto modo di apprezzare tutto ciò, dicevo, sapete di cosa parlo.
Ma come in tutti i matrimoni d’amore, si da il caso che i due coniugi non siano perfetti (altrimenti non sarebbe amore, ma convenienza, non trovate?).
Ed ecco che, dopo tanti anni di sviluppo con il mio linguaggio e il mio database preferito, mi accorgo pochi giorni fa di un bug che affligge la libreria InterBase fornita in stock con RAD Studio 2010, e che io non esito a definire colossale. La faccio breve: i TField associati ad un oggetto TIBDataSet non reagiscono correttamente al metodo Clear, a meno che siano del tipo TIBStringField, e come conseguenza può risultare impossibile attribuire loro il valore NULL.
Non mi credete, vero? Seguitemi.
Iniziamo con il creare un database, e al suo interno una semplice tabella:
create generator g_tabella; create table tabella ( id int, x bigint ); commit;
Creiamo poi una semplice applicazione VCL; buttiamo sulla form un oggetto TIBDatabase, una TIBTransaction, un TIBDataSet e un bel bottone. Facciamo puntare il nostro TIBDatabase al database appena creato, colleghiamo la TIBTransaction con il database e il dataset.
Settiamo le query dell’oggetto TIBDataSet:
SelectSQL -> SELECT ID, X FROM TABELLA
InsertSQL -> INSERT INTO TABELLA (ID, X) VALUES (:ID, :X)
ModifySQL -> UPDATE TABELLA SET ID = :ID, X = :X WHERE ID = :OLD_ID
DeleteSQL -> DELETE FROM TABELLA WHERE ID = :OLD_ID
RefreshSQL -> SELECT ID, X FROM TABELLA WHERE ID = :ID
Infine settiamo il GeneratorField del dataset.
Segue il listato del file DFM, per i più pigri:
object Form2: TForm2 Left = 0 Top = 0 Caption = 'Form2' ClientHeight = 226 ClientWidth = 400 Color = clBtnFace Font.Charset = DEFAULT_CHARSET Font.Color = clWindowText Font.Height = -11 Font.Name = 'Tahoma' Font.Style = [] OldCreateOrder = False PixelsPerInch = 96 TextHeight = 13 object Button1: TButton Left = 88 Top = 112 Width = 209 Height = 57 Caption = 'Button1' TabOrder = 0 OnClick = Button1Click end object IBDataSet1: TIBDataSet Database = IBDatabase1 Transaction = IBTransaction1 DeleteSQL.Strings = ( 'DELETE FROM TABELLA WHERE ID = :OLD_ID') InsertSQL.Strings = ( 'INSERT INTO TABELLA (ID, X) VALUES (:ID, :X)') RefreshSQL.Strings = ( 'SELECT ID, X FROM TABELLA WHERE ID = :ID') SelectSQL.Strings = ( 'SELECT ID, X FROM TABELLA') ModifySQL.Strings = ( 'UPDATE TABELLA SET ID = :ID, X = :X WHERE ID = :OLD_ID') GeneratorField.Field = 'ID' GeneratorField.Generator = 'G_TABELLA' Left = 264 Top = 48 object IBDataSet1ID: TIntegerField FieldName = 'ID' Origin = '"TABELLA"."ID"' end object IBDataSet1X: TLargeintField FieldName = 'X' Origin = '"TABELLA"."X"' end end object IBDatabase1: TIBDatabase Connected = True DatabaseName = 'D:\fbdata\TEST.FDB' Params.Strings = ( 'user_name=SYSDBA' 'password=masterkey' 'lc_ctype=UTF8') LoginPrompt = False AfterConnect = IBDatabase1AfterConnect BeforeDisconnect = IBDatabase1BeforeDisconnect Left = 96 Top = 48 end object IBTransaction1: TIBTransaction Active = True DefaultDatabase = IBDatabase1 Params.Strings = ( 'read_committed' 'rec_version' 'nowait') Left = 184 Top = 48 end end
Assegniamo un handler all’evento OnClick del bottone:
procedure TForm2.Button1Click(Sender: TObject); begin IBDataSet1.Append; IBDataSet1X.AsLargeInt := 12345; IBDataSet1.Post; ShowMessage('X ha valore ' + IntToStr(IBDataSet1X.AsLargeInt)); IBDataSet1.Edit; IBDataSet1X.Clear; IBDataSet1.Post; if IBDataSet1X.IsNull then ShowMessage('X ha ora valore NULL') else ShowMessage('X ha ancora valore ' + IntToStr(IBDataSet1X.AsLargeInt) + '!!!'); end;
Ecco cosa stiamo facendo nel metodo OnClick: creiamo un nuovo record, assegniamo il valore 12345 al campo X, salviamo. Quindi riapriamo il record in modifica, tentiamo di rendere NULL il campo X, salviamo. Dovremmo ottenere il messaggio “X ha ora valore NULL”, e invece ecco cosa accade:
Per scoprire cosa sta accadendo, dobbiamo addentrarci nel labirinto di chiamate innescate dal metodo Clear. Con il debugger attivo non è difficile. È sufficiente compilare con i file DCU di debug, e saremo in grado di eseguire lo step dentro alle funzioni di libreria:
Con un po’ di pazienza, ecco che arriviamo alla procedura TIBCustomDataSet.InternalSetFieldData:
procedure TIBCustomDataSet.InternalSetFieldData(Field: TField; Buffer: Pointer); var Buff, TmpBuff: TRecordBuffer; begin Buff := GetActiveBuf; if Field.FieldNo < 0 then begin TmpBuff := Buff + FRecordSize + Field.Offset; Boolean(TmpBuff[0]) := LongBool(Buffer); if Boolean(TmpBuff[0]) then Move(Buffer^, TmpBuff[1], Field.DataSize); WriteRecordCache(PRecordData(Buff)^.rdRecordNumber, Buff); end else begin CheckEditState; with PRecordData(Buff)^ do begin { If inserting, Adjust record position } AdjustRecordOnInsert(Buff); if (FMappedFieldPosition[Field.FieldNo - 1] > 0) and (FMappedFieldPosition[Field.FieldNo - 1] <= rdFieldCount) then begin Field.Validate(Buffer); if (Buffer = nil) or (Field is TIBStringField) and (PChar(Buffer)[0] = #0) then if TIBStringField(Field).EmptyAsNull then rdFields[FMappedFieldPosition[Field.FieldNo - 1]].fdIsNull := True else begin rdFields[FMappedFieldPosition[Field.FieldNo - 1]].fdDataLength := 0; rdFields[FMappedFieldPosition[Field.FieldNo - 1]].fdIsNull := False; end else begin Move(Buffer^, Buff[rdFields[FMappedFieldPosition[Field.FieldNo - 1]].fdDataOfs], rdFields[FMappedFieldPosition[Field.FieldNo - 1]].fdDataSize); if (rdFields[FMappedFieldPosition[Field.FieldNo - 1]].fdDataType = SQL_TEXT) or (rdFields[FMappedFieldPosition[Field.FieldNo - 1]].fdDataType = SQL_VARYING) then rdFields[FMappedFieldPosition[Field.FieldNo - 1]].fdDataLength := StrLen(PChar(Buffer)) * 2; rdFields[FMappedFieldPosition[Field.FieldNo - 1]].fdIsNull := False; if rdUpdateStatus = usUnmodified then begin if CachedUpdates then begin FUpdatesPending := True; if State = dsInsert then rdCachedUpdateStatus := cusInserted else if State = dsEdit then rdCachedUpdateStatus := cusModified; end; if State = dsInsert then rdUpdateStatus := usInserted else rdUpdateStatus := usModified; end; WriteRecordCache(rdRecordNumber, Buff); SetModified(True); end; end; end; end; if not (State in [dsCalcFields, dsFilter, dsNewValue]) then DataEvent(deFieldChange, Longint(Field)); end;
L'occhio attento ha già scovato il grossolano errore. Ecco invece la spiegazione per i miopi:
... if (Buffer = nil) or (Field is TIBStringField) and (PChar(Buffer)[0] = #0) then if TIBStringField(Field).EmptyAsNull then rdFields[FMappedFieldPosition[Field.FieldNo - 1]].fdIsNull := True else begin rdFields[FMappedFieldPosition[Field.FieldNo - 1]].fdDataLength := 0; rdFields[FMappedFieldPosition[Field.FieldNo - 1]].fdIsNull := False; end else ...
Cosa succede se Buffer = nil (cosa che accade per esempio se si arriva qui dal metodo Clear) ma Field non è un TIBStringField? Succede che il cast statico TIBStringField(Field) non ha senso, e l'espressione TIBStringField(Field).EmptyAsNull sta restituendo chissà quale valore, visto che sta trattando come TIBStringField un oggetto che non lo è. Ecco perché all'inizio ho detto "può risultare impossibile attribuire loro il valore NULL": il comportamento di questa porzione di codice è alquanto imprevedibile.
Che fare? Abbiamo il sorgente, ma la lungimirante Embarcadero non ci ha fornito tutti i file necessari per fare un rebuild del package. E, se anche riuscissimo nell'impresa, non è detto che non finiamo con il rompere qualcos'altro, cosa molto probabile ad esempio se ci fosse qualche altro package che dipende da questo. Qualcuno ha mai provato a installare in RAD Studio 2010 un aggiornamento delle librerie Indy? Ecco, se ci avete provato sapete di cosa sto parlando.
Quality Central, dite? ROTFL. No, dobbiamo arrangiarci. Andreas Hausladen docet. Dopo lunghe e penose riflessioni , sono giunto ad una soluzione: creare un mio package, contenente un clone corretto del componente TIBDataSet. Mettiamoci al lavoro.
Innanzitutto, osserviamo che non ci interessa clonare l'intero componente. Ci basterebbe risalire nello stack delle chiamate partendo dalla funzione incriminata, fino a trovarne una che sia dichiarata virtual. Possiamo quindi creare una classe discendente da TIBDataSet e farne l'override, in modo da dirottare l'esecuzione sulla nostra versione di InternalSetFieldData.
Con poco sforzo, troviamo che le due funzioni da noi cercate si trovano proprio nel progenitore di tutti i dataset: TDataSet.
procedure SetFieldData(Field: TField; Buffer: Pointer); overload; virtual; procedure SetFieldData(Field: TField; Buffer: Pointer; NativeFormat: Boolean); overload; virtual;
L'override viene fatto nella classe TIBCustomDataSet, che è la superclasse più prossima di TIBDataSet:
procedure SetFieldData(Field : TField; Buffer : Pointer); override; procedure SetFieldData(Field : TField; Buffer : Pointer; NativeFormat : Boolean); overload; override;
Eccone l'implementazione:
procedure TIBCustomDataSet.SetFieldData(Field: TField; Buffer: Pointer); var lTempCurr : System.Currency; begin if (Field.DataType = ftBCD) and (Buffer <> nil) then begin BCDToCurr(TBCD(Buffer^), lTempCurr); InternalSetFieldData(Field, @lTempCurr); end else InternalSetFieldData(Field, Buffer); end; procedure TIBCustomDataSet.SetFieldData(Field: TField; Buffer: Pointer; NativeFormat: Boolean); begin if (not NativeFormat) and (Field.DataType = ftBCD) then InternalSetfieldData(Field, Buffer) else inherited SetFieldData(Field, buffer, NativeFormat); end;
Bingo! Ecco il grip di cui avevamo bisogno. Battezziamo il nostro nuovo componente TWrapIBDataSet, deriviamolo da TIBDataSet, dichiariamo tutte le proprietà published, dichiariamo l'override delle due funzioni e creiamo la nostra versione corretta di InternalSetFieldData:
unit IBWrapObjs; interface uses Classes, SysUtils, Windows, IBCustomDataSet, IBQuery, IBSQL, DB, IBDatabase, IBExternals, IBHeader, IBIntf, FMTBcd; type TWrapIBDataSet = class(TIBDataSet) published { TIBCustomDataSet } property BufferChunks; property CachedUpdates; property DeleteSQL; property InsertSQL; property RefreshSQL; property SelectSQL; property ModifySQL; property ParamCheck; property UniDirectional; property Filtered; property GeneratorField; property BeforeDatabaseDisconnect; property AfterDatabaseDisconnect; property DatabaseFree; property BeforeTransactionEnd; property AfterTransactionEnd; property TransactionFree; property UpdateObject; { TIBDataSet } property Active; property AutoCalcFields; property DataSource; property AfterCancel; property AfterClose; property AfterDelete; property AfterEdit; property AfterInsert; property AfterOpen; property AfterPost; property AfterScroll; property BeforeCancel; property BeforeClose; property BeforeDelete; property BeforeEdit; property BeforeInsert; property BeforeOpen; property BeforePost; property BeforeScroll; property OnCalcFields; property OnDeleteError; property OnEditError; property OnFilterRecord; property OnNewRecord; property OnPostError; protected procedure SetFieldData(Field : TField; Buffer : Pointer); override; procedure SetFieldData(Field : TField; Buffer : Pointer; NativeFormat : Boolean); overload; override; private procedure InternalSetFieldData(Field: TField; Buffer: Pointer); end; implementation { TWrapIBDataSet } procedure TWrapIBDataSet.SetFieldData(Field: TField; Buffer: Pointer); var lTempCurr : System.Currency; begin if (Field.DataType = ftBCD) and (Buffer <> nil) then begin BCDToCurr(TBCD(Buffer^), lTempCurr); InternalSetFieldData(Field, @lTempCurr); end else InternalSetFieldData(Field, Buffer); end; procedure TWrapIBDataSet.SetFieldData(Field: TField; Buffer: Pointer; NativeFormat: Boolean); begin if (not NativeFormat) and (Field.DataType = ftBCD) then InternalSetfieldData(Field, Buffer) else inherited SetFieldData(Field, buffer, NativeFormat); end; procedure TWrapIBDataSet.InternalSetFieldData(Field: TField; Buffer: Pointer); var Buff, TmpBuff: TRecordBuffer; isString: Boolean; begin Buff := GetActiveBuf; if Field.FieldNo < 0 then begin TmpBuff := Buff + FRecordSize + Field.Offset; Boolean(TmpBuff[0]) := LongBool(Buffer); if Boolean(TmpBuff[0]) then Move(Buffer^, TmpBuff[1], Field.DataSize); WriteRecordCache(PRecordData(Buff)^.rdRecordNumber, Buff); end else begin CheckEditState; with PRecordData(Buff)^ do begin { If inserting, Adjust record position } AdjustRecordOnInsert(Buff); if (FMappedFieldPosition[Field.FieldNo - 1] > 0) and (FMappedFieldPosition[Field.FieldNo - 1] <= rdFieldCount) then begin Field.Validate(Buffer); { //ecco la porzione di codice by Embarcadero... //se Buffer = nil ma Field NON è un TIBStringField, //allora TIBStringField(Field).EmptyAsNull chissà //a cosa diavolo sta puntando... if (Buffer = nil) or (Field is TIBStringField) and (PChar(Buffer)[0] = #0) then if TIBStringField(Field).EmptyAsNull then rdFields[FMappedFieldPosition[Field.FieldNo - 1]].fdIsNull := True else begin rdFields[FMappedFieldPosition[Field.FieldNo - 1]].fdDataLength := 0; rdFields[FMappedFieldPosition[Field.FieldNo - 1]].fdIsNull := False; end else } isString := Field is TIBStringField; if (Buffer = nil) or isString and (PChar(Buffer)[0] = #0) then if not isString or TIBStringField(Field).EmptyAsNull then rdFields[FMappedFieldPosition[Field.FieldNo - 1]].fdIsNull := True else begin rdFields[FMappedFieldPosition[Field.FieldNo - 1]].fdDataLength := 0; rdFields[FMappedFieldPosition[Field.FieldNo - 1]].fdIsNull := False; end else begin Move(Buffer^, Buff[rdFields[FMappedFieldPosition[Field.FieldNo - 1]].fdDataOfs], rdFields[FMappedFieldPosition[Field.FieldNo - 1]].fdDataSize); if (rdFields[FMappedFieldPosition[Field.FieldNo - 1]].fdDataType = SQL_TEXT) or (rdFields[FMappedFieldPosition[Field.FieldNo - 1]].fdDataType = SQL_VARYING) then rdFields[FMappedFieldPosition[Field.FieldNo - 1]].fdDataLength := StrLen(PChar(Buffer)) * 2; rdFields[FMappedFieldPosition[Field.FieldNo - 1]].fdIsNull := False; if rdUpdateStatus = usUnmodified then begin if CachedUpdates then begin FUpdatesPending := True; if State = dsInsert then rdCachedUpdateStatus := cusInserted else if State = dsEdit then rdCachedUpdateStatus := cusModified; end; if State = dsInsert then rdUpdateStatus := usInserted else rdUpdateStatus := usModified; end; WriteRecordCache(rdRecordNumber, Buff); SetModified(True); end; end; end; end; if not (State in [dsCalcFields, dsFilter, dsNewValue]) then DataEvent(deFieldChange, Longint(Field)); end; end.
Creiamo un package di runtime:
package IBWrapper; {$R *.res} {$ALIGN 8} {$ASSERTIONS ON} {$BOOLEVAL OFF} {$DEBUGINFO ON} {$EXTENDEDSYNTAX ON} {$IMPORTEDDATA ON} {$IOCHECKS ON} {$LOCALSYMBOLS ON} {$LONGSTRINGS ON} {$OPENSTRINGS ON} {$OPTIMIZATION ON} {$OVERFLOWCHECKS OFF} {$RANGECHECKS OFF} {$REFERENCEINFO ON} {$SAFEDIVIDE OFF} {$STACKFRAMES OFF} {$TYPEDADDRESS OFF} {$VARSTRINGCHECKS ON} {$WRITEABLECONST OFF} {$MINENUMSIZE 1} {$IMAGEBASE $400000} {$DESCRIPTION 'Interbase Objects Wrappers Runtime Package'} {$RUNONLY} {$IMPLICITBUILD ON} requires ibxpress, rtl; contains IBWrapObjs in 'IBWrapObjs.pas'; end.
Poi creiamo il design time package:
package dclIBWrapper; {$R *.res} {$ALIGN 8} {$ASSERTIONS ON} {$BOOLEVAL OFF} {$DEBUGINFO ON} {$EXTENDEDSYNTAX ON} {$IMPORTEDDATA ON} {$IOCHECKS ON} {$LOCALSYMBOLS ON} {$LONGSTRINGS ON} {$OPENSTRINGS ON} {$OPTIMIZATION ON} {$OVERFLOWCHECKS OFF} {$RANGECHECKS OFF} {$REFERENCEINFO ON} {$SAFEDIVIDE OFF} {$STACKFRAMES OFF} {$TYPEDADDRESS OFF} {$VARSTRINGCHECKS ON} {$WRITEABLECONST OFF} {$MINENUMSIZE 1} {$IMAGEBASE $400000} {$DESCRIPTION 'Interbase Objects Wrappers Designtime Package'} {$DESIGNONLY} {$IMPLICITBUILD ON} requires IBWrapper, designide, rtl, dclib; contains IBWrapReg in 'IBWrapReg.pas'; end.
unit IBWrapReg; interface uses IBWrapObjs, Classes, DesignIntf, DesignEditors, StrEdit, DBReg, SysUtils, IBUpdateSQLEditor, IBWrapEdit, Controls, Forms, IBXConst; procedure Register; implementation procedure Register; begin RegisterComponents('Interbase (Wrapper)', [TWrapIBDataSet]); end; end.
Non ci resta che compilare e installare il package. Giusto? Sbagliato, ovviamente. Il compilatore da 57 errori, o giù di lì. Perché? Be', abbiamo fatto i conti senza l'oste: noi non abbiamo accesso ai campi e ai metodi privati di TIBCustomDataSet che ci servirebbero nella nostra versione customizzata di InternalSetFieldData. FRecordSize, FMappedFieldPosition, eccetera per noi sono inaccessibili.
Inaccessibili?!? Questa parola non esiste. Noi non ci arrendiamo. Chi si arrende è un pappamolla, un formaggino, una vongola senza guscio!
Dunque, sappiamo che per Delphi (a differenza del C++) la parola chiave private ha effetto solo al di fuori della unit in cui è utilizzata. Dentro la medesima unit, nulla è privato. Possiamo anche sapere qual è la posizione relativa dei campi che ci interessano in relazione al puntatore Self; lo sappiamo perché abbiamo il sorgente di TIBCustomDataSet.
Possiamo quindi creare una nostra classe locale, che abbia il medesimo layout in memoria di TIBCustomDataSet, e accedere ai campi privati attraverso di essa.
Ecco come:
{ creiamo una nostra classe TIBCustomDataSetHack che è identica a TIBCustomDataSet. } type {$HINTS OFF} TIBCustomDataSetHack = class(TWideDataSet) private FNeedsRefresh: Boolean; FForcedRefresh: Boolean; FIBLoaded: Boolean; FBase: TIBBase; FBlobCacheOffset: Integer; FBlobStreamList: TList; FBufferChunks: Integer; FBufferCache, FOldBufferCache: TRecordBuffer; FBufferChunkSize, FCacheSize, FOldCacheSize: Integer; FFilterBuffer: TRecordBuffer; FBPos, FOBPos, FBEnd, FOBEnd: DWord; FCachedUpdates: Boolean; FCalcFieldsOffset: Integer; FCurrentRecord: Long; FDeletedRecords: Long; FModelBuffer, FOldBuffer, FTempBuffer: TRecordBuffer; FOpen: Boolean; FInternalPrepared: Boolean; FQDelete, FQInsert, FQRefresh, FQSelect, FQModify: TIBSQL; FRecordBufferSize: Integer; FRecordCount: Integer; FRecordSize: Integer; FUniDirectional: Boolean; FUpdateMode: TUpdateMode; FUpdateObject: TIBDataSetUpdateObject; FParamCheck: Boolean; FUpdatesPending: Boolean; FUpdateRecordTypes: TIBUpdateRecordTypes; FMappedFieldPosition: array of Integer; FDataLink: TIBDataLink; FStreamedActive : Boolean; FLiveMode: TLiveModes; FGeneratorField: TIBGeneratorField; FRowsAffected: Integer; FBeforeDatabaseDisconnect, FAfterDatabaseDisconnect, FDatabaseFree: TNotifyEvent; FOnUpdateError: TIBUpdateErrorEvent; FOnUpdateRecord: TIBUpdateRecordEvent; FBeforeTransactionEnd, FAfterTransactionEnd, FTransactionFree: TNotifyEvent; FGDSLibrary : IGDSLibrary; end; {$HINTS ON}
Notare l'uso della direttiva {$HINTS OFF} per mettere a tacere gli warning del compilatore in questa porzione di codice.
Ci resta da fare solo una cosa: trovare il modo di accedere ai metodi privati, ovvero TIBCustomDataSet.CheckEditState, TIBCustomDataSet.AdjustRecordOnInsert e TIBCustomDataSet.WriteRecordCache.
In questo difficile compito ci vengono in aiuto i class helper. I class helper sono dei costrutti che ci consentono di estendere una classe esistente, aggiungendo metodi e campi a piacimento. Possiamo quindi definire un class helper per TIBCustomDataSet, che ci consenta di scovare l'indirizzo delle funzioni di nostro interesse. Poi, con un po' di assembler, inseriremo le chiamate nella nostra funzione patchata. Ecco come:
unit IBWrapObjs; interface ... type TIBCustomDataSetHelper = class helper for TIBCustomDataSet function GetWriteRecordCacheAddress: Pointer; function GetCheckEditStateAddress: Pointer; function GetAdjustRecordOnInsertAddress: Pointer; end; implementation ... type TWriteRecordCache = procedure(RecordNumber: Integer; Buffer: TRecordBuffer) of object; TCheckEditState = procedure of object; TAdjustRecordOnInsert = procedure(Buffer: Pointer) of object; var fnWriteRecordCache: Pointer; fnCheckEditState: Pointer; fnAdjustRecordOnInsert: Pointer; function TIBCustomDataSetHelper.GetAdjustRecordOnInsertAddress: Pointer; var MethodPtr: TAdjustRecordOnInsert; begin MethodPtr := Self.AdjustRecordOnInsert; Result := TMethod(MethodPtr).Code; end; function TIBCustomDataSetHelper.GetCheckEditStateAddress: Pointer; var MethodPtr: TCheckEditState; begin MethodPtr := Self.CheckEditState; Result := TMethod(MethodPtr).Code; end; function TIBCustomDataSetHelper.GetWriteRecordCacheAddress: Pointer; var MethodPtr: TWriteRecordCache; begin MethodPtr := Self.WriteRecordCache; Result := TMethod(MethodPtr).Code; end; initialization fnWriteRecordCache := TIBCustomDataSet(nil).GetWriteRecordCacheAddress; fnCheckEditState := TIBCustomDataSet(nil).GetCheckEditStateAddress; fnAdjustRecordOnInsert := TIBCustomDataSet(nil).GetAdjustRecordOnInsertAddress; end.
Bene, non ci resta che definire tre funzioni private: CheckEditState, AdjustRecordOnInsert, WriteRecordCache. Esse chiameranno, usando un po' di assembler, le vere funzioni implementate in TIBCustomDataSet:
type TWrapIBDataSet = class(TIBDataSet) ... private procedure AdjustRecordOnInsert(Buffer: Pointer); procedure CheckEditState; procedure WriteRecordCache(RecordNumber: Integer; Buffer: TRecordBuffer); end;
Ecco l'implementazione:
procedure TWrapIBDataSet.AdjustRecordOnInsert(Buffer: Pointer); asm push EAX push EDX mov EAX, Self mov EDX, Buffer call fnAdjustRecordOnInsert pop EDX pop EAX end; procedure TWrapIBDataSet.CheckEditState; asm push EAX mov EAX, Self call fnCheckEditState pop EAX end; procedure TWrapIBDataSet.WriteRecordCache(RecordNumber: Integer; Buffer: TRecordBuffer); asm push EAX push EDX push ECX mov EAX, Self mov EDX, RecordNumber mov ECX, Buffer call fnWriteRecordCache pop ECX pop EDX pop EAX end;
Ora modifichiamo InternalSetFieldData come segue:
procedure TWrapIBDataSet.InternalSetFieldData(Field: TField; Buffer: Pointer); var Buff, TmpBuff: TRecordBuffer; hack: TIBCustomDataSetHack; isString: Boolean; begin hack := TIBCustomDataSetHack(Self); Buff := GetActiveBuf; if Field.FieldNo < 0 then begin TmpBuff := Buff + hack.FRecordSize + Field.Offset; Boolean(TmpBuff[0]) := LongBool(Buffer); if Boolean(TmpBuff[0]) then Move(Buffer^, TmpBuff[1], Field.DataSize); WriteRecordCache(PRecordData(Buff)^.rdRecordNumber, Buff); end else begin CheckEditState; with PRecordData(Buff)^ do begin { If inserting, Adjust record position } AdjustRecordOnInsert(Buff); if (hack.FMappedFieldPosition[Field.FieldNo - 1] > 0) and (hack.FMappedFieldPosition[Field.FieldNo - 1] <= rdFieldCount) then begin Field.Validate(Buffer); { //ecco la porzione di codice by Embarcadero... //se Buffer = nil ma Field NON è un TIBStringField, //allora TIBStringField(Field).EmptyAsNull chissà //a cosa diavolo sta puntando... if (Buffer = nil) or (Field is TIBStringField) and (PChar(Buffer)[0] = #0) then if TIBStringField(Field).EmptyAsNull then rdFields[FMappedFieldPosition[Field.FieldNo - 1]].fdIsNull := True else begin rdFields[FMappedFieldPosition[Field.FieldNo - 1]].fdDataLength := 0; rdFields[FMappedFieldPosition[Field.FieldNo - 1]].fdIsNull := False; end else } isString := Field is TIBStringField; if (Buffer = nil) or isString and (PChar(Buffer)[0] = #0) then //trattiamo in modo speciale solo i TIBStringField if not isString or TIBStringField(Field).EmptyAsNull then rdFields[hack.FMappedFieldPosition[Field.FieldNo - 1]].fdIsNull := True else begin rdFields[hack.FMappedFieldPosition[Field.FieldNo - 1]].fdDataLength := 0; rdFields[hack.FMappedFieldPosition[Field.FieldNo - 1]].fdIsNull := False; end else begin Move(Buffer^, Buff[rdFields[hack.FMappedFieldPosition[Field.FieldNo - 1]].fdDataOfs], rdFields[hack.FMappedFieldPosition[Field.FieldNo - 1]].fdDataSize); if (rdFields[hack.FMappedFieldPosition[Field.FieldNo - 1]].fdDataType = SQL_TEXT) or (rdFields[hack.FMappedFieldPosition[Field.FieldNo - 1]].fdDataType = SQL_VARYING) then rdFields[hack.FMappedFieldPosition[Field.FieldNo - 1]].fdDataLength := StrLen(PChar(Buffer)) * 2; rdFields[hack.FMappedFieldPosition[Field.FieldNo - 1]].fdIsNull := False; if rdUpdateStatus = usUnmodified then begin if CachedUpdates then begin hack.FUpdatesPending := True; if State = dsInsert then rdCachedUpdateStatus := cusInserted else if State = dsEdit then rdCachedUpdateStatus := cusModified; end; if State = dsInsert then rdUpdateStatus := usInserted else rdUpdateStatus := usModified; end; WriteRecordCache(rdRecordNumber, Buff); SetModified(True); end; end; end; end; if not (State in [dsCalcFields, dsFilter, dsNewValue]) then DataEvent(deFieldChange, Longint(Field)); end;
Visto cosa abbiamo fatto? Abbiamo preso un riferimento a noi stessi (Self) e abbiamo detto al compilatore di trattarlo come se fosse un TIBCustomDataSetHack. A questo punto possiamo accedere a tutti i campi che desideriamo attraverso il puntatore a hack; le locazioni di memoria che raggiungiamo sono quelle corrette (quelle relative ai campi di TIBCustomDataSet, non di TIBCustomDataSetHack), perché un TIBCustomDataSet ha una forma identica a quella di un TIBCustomDataSetHack, ma noi stiamo lavorando su un oggetto che è, in effetti, un TIBCustomDataSet!
Inoltre, grazie all'helper, abbiamo recuperato l'indirizzo delle funzioni che ci servivano, e le abbiamo chiamate "abusivamente" usando un po' di assembler.
Bene, compiliamo il runtime package e installiamo il design time package nell'IDE.
Ora riprendiamo il nostro progetto di test, e sostituiamo il TIBDataSet con un TWrapIBDataSet. Possiamo usare un editor di testo sul file .pas e sul file .dfm, per una sostituzione veloce.
Nel file dfm:
... object IBDataSet1: TWrapIBDataSet ...
Nel file pas:
... type TForm2 = class(TForm) IBDataSet1: TWrapIBDataSet; ...
Compiliamo il test, eseguiamo:
Vittoria! Abbiamo dovuto adottare un paio di trucchi piuttosto sporchi, ma come si dice: in guerra e in amore tutto è lecito!
Buona programmazione a tutti.