Delphi: Dll -> DataSet.First causes Access Violation

150 Views Asked by At

I have a simple Dll written in Delphi:

library usr_d;

uses
  System.SysUtils, System.Classes, DB,Vcl.Dialogs
  ;

{$R *.res}

Procedure SetMyData(DataSet: TDataSet);export;
begin
  if Assigned(DataSet) then
  begin
    ShowMessage(DataSet.FieldByName('MyFieldName1').AsString);
    try
      DataSet.First;
    except on E: Exception do
      ShowMessage('Error accessing field: ' + E.Message);
    end;
  end
  else
    ShowMessage('DataSet parameter is not assigned!');
end;

exports
  SetMyData;

begin
end.    

I call it this way:

Procedure TMainForm.CallMyDll;

type
  TSetMyData = procedure(DataSet: TDataSet); stdcall;

Var
  MyHandle               : HMODULE;
  SetMyData              : TSetMyData;  

Begin
  MyHandle := LoadLibrary('c:\MyFolder\usr_d.dll');
  if MyHandle <> 0 then
   begin
     @SetMyData := GetProcAddress(MyHandle, 'SetMyData');
     if @SetMyData <> nil then
      begin
        SetMyData(MyQuery as TDataSet);
      end;
     FreeLibrary(MyHandle);
   end;
End;

When I run the program and call the procedure of the Dll, the "ShowMessage(DataSet.FieldByName('MyFieldName1').AsString);" works correctly, it shows the field value. I tried it with a few different datasets.

However if I issue: "DataSet.First;" or "DataSet.Next;", then I get Access Violation, error accessing field.

What should I do? Thank you!

2

There are 2 best solutions below

0
Arthur Hoornweg On BEST ANSWER

As previous respondents have explained, you cannot safely pass a Delphi object across exe/dll boundaries.

The reasons?

First of all, the exe and the dll have separate heap managers which will wreak total havoc because each of them thinks it is solely responsible for allocating and deallocating memory. There are ways to make them share a heap manager - google sharemem or simplesharemem.

Secondly, the Delphi smart linker removes any methods that aren't used in the exe but also does the same thing for the dll. So the memory layout of tDataset will be different in both modules which will most likely result in access violations. The only way to overcome that is by using packages.

But having said that, there are some datasets that can easily be shared between executable and DLL, for example if they are based on COM objects. For example, tAdodataset has a property "recordset" that contains the underlying data and tAdoConnection has a property "connectionobject" that contains the underlying database connection.

What I mean is, if a tAdoConnection in a DLL uses the same "connectionobject" as a tAdoconnection in an EXE, they basically share the same connection.

0
Remy Lebeau On

When making a plain DLL, you can only exchange trivial C-compatible types (ie, integers, characters, etc) across the DLL boundary. You simply cannot use non-trivial data, such as Delphi RTL/VCL objects, across the DLL boundary in a plain DLL.

You need to create a Package (BPL) instead, which is a special kind of DLL that has built-in support for the RTL/VCL frameworks.

And, in order to share Delphi objects over the DLL boundary in a Package, you will also need to have both the BPL project and the calling project (whether that be an EXE or another BPL) enable Runtime Packages so that they can share a single instance of the RTL/VCL framework implementation, the memory manager, RTTI, etc.


If redesigning your DLL into a Package is not an option, then you will have to change your whole approach. For example, you could use callback functions instead, so that everything related to your objects stays on one side of the DLL boundary, eg:

MyCommon.pas

type
  TMyCallbackFuncs = record
    GetFieldStr: function(FieldName, Buffer: PChar; BufLen: Integer; UserData: Pointer): Integer; stdcall;
    SetDataToFirst: procedure(UserData: Pointer); stdcall;
  end;

usr_d.pas

library usr_d;

uses
  System.SysUtils, System.Classes, Vcl.Dialogs, MyCommon;

{$R *.res}

procedure SetMyData(var Callbacks: TMyCallbackFuncs; UserData: Pointer); stdcall; export;
var
  Value: array[0..255] of Char;
begin
  try
    Callbacks.GetFieldStr('MyFieldName1', Value, Length(Value), UserData);
    ShowMessage(Value);
    Callbacks.SetDataToFirst(UserData);
  except
    ShowMessage('Error accessing field');
  end;
end;

exports
  SetMyData;

begin
end.    

CallMyDll.pas

uses
  ..., MyCommon;

function GetFieldStr(FieldName, Buffer: PChar; BufLen: Integer; UserData: Pointer): Integer; stdcall;
begin
  StrPLCopy(Buffer, TDataSet(UserData).FieldByName(FieldName).AsString, BufLen-1);
  Result := StrLen(Buffer);
end;

procedure SetDataToFirst(UserData: Pointer); stdcall;
begin
  TDataSet(UserData).First;
end;

procedure TMainForm.CallMyDll;
type
  TSetMyData = procedure(var Callbacks: TCallbackFuncs; UserData: Pointer); stdcall;
var
  MyHandle  : HMODULE;
  SetMyData : TSetMyData;  
  Callbacks : TMyCallbackFuncs;
begin
  MyHandle := LoadLibrary('c:\MyFolder\usr_d.dll');
  if MyHandle <> 0 then
  begin
    @SetMyData := GetProcAddress(MyHandle, 'SetMyData');
    if @SetMyData <> nil then
    begin
      Callbacks.GetFieldStr := @GetFieldStr;
      Callbacks.SetDataToFirst := @SetDataToFirst;
      SetMyData(Callbacks, MyQuery as TDataSet);
    end;
    FreeLibrary(MyHandle);
  end;
end;