从Delphi DLL中导出一个全局符号

12

我正在尝试在Delphi中创建一个兼容Gecko 2.0的DLL。

以前(在Gecko 2.0之前),DLL需要导出NSGetModule()函数。这个方法非常有效。

从Firefox 4开始,我的DLL被加载了(我已经通过初始化部分的断点验证了这一点),但是我的NSGetModule()函数不再被调用了。这是设计行为,因为从Gecko 2.0(Firefox 4)开始,二进制组件不应该导出NSGetModule()函数:

https://developer.mozilla.org/en/XPCOM/XPCOM_changes_in_Gecko_2.0#Binary_components

根据这些文档,我的DLL需要导出一个指向结构体的NSModule数据符号。在Delphi术语中,我认为这是一个指向Delphi记录的全局变量。

在C++中,这是导出(全局)数据符号的方法:

define NSMODULE_DEFN(_name) extern "C" NS_EXPORT mozilla::Module const *const NSModule

我的问题:如何在Delphi中完成这个操作?如何导出全局变量?

感谢您的反馈。

5个回答

15

Delphi以类似导出函数的方式从DLL中导出全局变量:

library exp;
var
  global: Integer;
exports global;
end.

在Delphi中,可以从DLL中导入全局变量,但这有点像一个技巧:声明一个同名于要导入的全局变量的DLL导入过程,然后获取该过程的地址并适当地调整它。从Delphi的角度来看,DLL导入的过程是通过DLL导入表进行间接跳转的存根。导出的变量由操作系统加载器连接,将导出的全局变量的地址放置在导入表中,几乎与导出过程的地址被相似地修改一样。

例如:

{$apptype console}

procedure global; external 'exp.dll';

function GetGlobalAddr: PInteger;
type
  PPPointer = ^PPointer;
var
  p: PByte;
begin
  p := @global;
  Assert(p^ = $FF); // $FF $25 => indirect jump m32
  Inc(p);
  Assert(p^ = $25);
  Inc(p);
  Result := PPPointer(p)^^
end;

begin
  Writeln(GetGlobalAddr^);
end.
当然,后面的细节是实现和平台相关等等。可能更安全的方法是使用LoadLibraryGetProcAddress,当传递变量名时将返回全局变量的地址。当然,这也与平台有关。
64位Windows上,代码略有不同。操作码相同,但对于相同指令序列的寻址方式不同;不再是32位绝对偏移量,而是32位相对偏移量。
function GetGlobalAddr: PInteger;
type
  PPPointer = ^PPointer;
var
  p: PByte;
  ofs: Integer;
begin
  p := @global;
  Assert(p^ = $FF); // $FF $25 => indirect jump m32
  Inc(p);
  Assert(p^ = $25);
  Inc(p);
  // 32-bit offset follows
  ofs := PInteger(p)^;
  // offset is relative to next instruction
  Inc(p, SizeOf(ofs) + ofs);
  Result := PPPointer(p)^^
end;

这个安全吗?兼容64位吗?未来的Delphi版本呢? - FLICKER

2
阅读文档后,我认为Delphi不允许直接导出全局变量,因为exports语句的帮助只涉及例程。此外,有一个非常明确的引用:“在共享库中声明的全局变量不能被Delphi应用程序导入。”可以安全地假设如果Delphi不能导入它们,那么它也不会导出它们。我认为解决这个问题的方法可能是导出一个返回指向全局变量的指针的函数...类似于:
type
  RGlobalRecord = record
    ...
  end;
  PGlobalRecord = ^RGlobalRecord;

var
  _GlobalRecord: RGlobalRecord;

function GetGlobalRecord: PGlobalRecord;
begin
  Result := @_GlobalRecord;
end;

exports GetGlobalRecord name 'ExternalNameOfGlobalRecord';

因此,如果NSGetModule函数返回的结构与您现在需要导出为全局变量的结构相同,则可以尝试使用所需的名称导出该函数作为全局变量:

exports NSGetModule name 'NSModule';

2
这个函数会在地址前放置一个mov指令,而Gecko 2可能希望在该地址处有一个结构体,而不是代码或指针...也许如果您将该函数定义为注册函数并使用一个asm块,然后可以直接使用dd、dw、db来Poke信息。这很原始,看起来很糟糕,但可能有效。我也没有看到其他方法。 - Ritsaert Hornstra
@Ritsaert:谢谢,我很乐意承认我没有使用跨语言dll的经验。如果您有示例,请添加您自己的答案并获得一些声望。 - Marjan Venema
2
我认为这不会行。最合理的做法是使用具有小型运行时(例如ansi C)的编译器,该编译器可以导出变量,并将Pascal变量导入C中(静态,没有DLL),然后从DLL中导出它。然后将Pascal和C模块链接到一个DLL中。Free Pascal具有CVAR并且可以声明外部变量,但我不知道是否与DLL一起使用。无论如何,据我所知,从DLL中导出变量不受MS的官方支持(尽管许多DLL会这样做)。最好是引起Gecko的注意,并将其更改为函数。 - Marco van de Voort
1
这里描述的技术不起作用。可能有效的是使用具有足够“dd”或类似数据条目的asm块来填充结构的预期长度,以及在DLL主要开始/结束处进行VirtualProtect,使包含结构的代码页可写。但这并不是必需的,因为Delphi直接支持导出变量(尽管我不知道该功能是何时引入的)。 - Barry Kelly
@Barry:谢谢。我最初阅读的文档是D2009的。D2010甚至在索引中都没有“exports”。D2010 docwiki关于导出语句的页面也没有提到导出变量...如果它比Delphi XE更早可用,那么它一定被隐藏得很好 :-) - Marjan Venema
显示剩余3条评论

1
这是我的 Delphi 解决方案。它甚至可以在 D5 中运行 :)
function MyComponentConstructor(aOuter: nsISupports; const IID: TGUID; out _result): nsresult; cdecl;
begin
  /* constructor */
end;


type
  TCIDEntry = record
    cid: ^TGUID;
    service: Boolean;
    getFactoryProc: Pointer;
    constructorProc: Pointer;
  end;

  TContractIDEntry = record
    constractid: PChar;
    cid: ^TGUID;
  end;

  TCategoryEntry = record
    category: PChar;
    entry: PChar;
    value: PChar;
  end;

  TModule = packed record
    mVersion: DWord;
    mCIDs: array of TCIDEntry;
    mContractIDs: array of TContractIDEntry;
    mCategoryEntries: array of TCategoryEntry;
    getFactoryProc: Pointer;
    loadProc: Pointer;
    unloadProc: Pointer;
  end;

  PModule = ^TModule;
  PPModule = ^PModule;

var
  mCIDs: array [0..1] of TCIDEntry =
  (
    ( cid: @Sample_cid; service: False; getFactoryProc: nil; constructorProc: @MyComponentConstructor ),
    ( cid: nil; service: False; getFactoryProc: nil; constructorProc: nil )
  );

  mContractIDs: array [0..1] of TContractIDEntry =
  (
    ( constractid: Sample_CONTRACTID; cid: @Sample_cid ),
    ( constractid: nil; cid: nil )
  );

  mCategoryEntries: array [0..2] of TCategoryEntry =
  (
    ( category: 'JavaScript-global-property'; entry: 'MyComponent'; value: Sample_CONTRACTID ),
    ( category: 'JavaScript-global-constructor'; entry: 'MyComponent'; value: Sample_CONTRACTID ),
    ( category: nil; entry: nil; value: nil )
  );

  NSModuleElem: TModule =
    (
       mVersion: 1;
       mCIDs: @mCIDs;
       mContractIDs: @mContractIDs;
       mCategoryEntries: @mCategoryEntries;
       getFactoryProc: nil;
       loadProc: nil;
       unloadProc: nil
    );

  NSModule: PModule = Addr(NSModuleElem);

exports
  NSModule name 'NSModule';

如果您能够提供Delphi中的GenericClassInfo实现,那就太棒了:)


0

这是我的当前实现(在FF 5和FF 6中可以工作,可能在以后的所有版本中都可以)

type
  TCIDEntry = record
    CID: PGUID;
    Service: BOOL;
    GetFactoryProc: Pointer;
    ConstructorProc: Pointer;
  end;

  TContract = record
    ContractID: PChar;
    CID: PGUID;
  end;

  TCategory = record
    Category: PChar;
    Entry: PChar;
    Value: PChar;
  end;

  TModule = record
    Version: UINT;
    CIDs: Pointer;
    Contracts: Pointer;
    Categories: Pointer;
    GetFactory: Pointer;
    Load: Pointer;
    Unload: Pointer;
  end;

  PModule = ^TModule;

var
  NSModule: PModule;

implementation

var
  mtModule: TModule;
  CIDs: array[0..1] of TCIDEntry;
  Contracts: array[0..1] of TContract;

function GetFileVersionResourceInfo(const FileName, VerValue: string): string;
var
  S: string;
  Value: Pointer;
  ValueSize: DWORD;
  VerInfoSize: DWORD;
  VersionInfo: Pointer;
  GetInfoSizeJunk: DWORD;
begin
  // retrieve the size of the version information resource
  VerInfoSize := GetFileVersionInfoSize(PChar(FileName), GetInfoSizeJunk);
  if VerInfoSize > 0 then
  begin
    // retrieve memory to hold the version resource
    GetMem(VersionInfo, VerInfoSize);
    try
      // retrieve the version resource
      if GetFileVersionInfo(PChar(FileName), 0, VerInfoSize, VersionInfo) then
        if VerQueryValue(VersionInfo, '\\VarFileInfo\\Translation', Value, ValueSize) then
        begin
          S := '\\StringFileInfo\\' +
          IntToHex(LoWord(LongInt(Value^)), 4) +
          IntToHex(HiWord(LongInt(Value^)), 4) + '\\';
          if VerQueryValue(VersionInfo, PChar(S + VerValue), Value, ValueSize) then Result := PChar(Value);
        end;
    finally
      FreeMem(VersionInfo, VerInfoSize);
    end;
  end;
end;

function GetVersion: Integer;
var
  I: Integer;
  sProductVersion: string;
  sModuleFileName: array[0..MAX_PATH] of Char;
begin
  Result := 1; // Firefox 4
  FillChar(sModuleFileName, MAX_PATH, 0);
  if GetModuleFileName(0, sModuleFileName, SizeOf(sModuleFileName)) > 0 then
  begin
    sProductVersion := Trim(GetFileVersionResourceInfo(sModuleFileName, 'ProductVersion'));
    if (sProductVersion <> '') and (sProductVersion[1] in ['4'..'9']) then
    begin
      // Firefox 4 = version 1
      // Firefox 5 = version 2
      // Firefox 6 = version 6
      // etc.
      I := StrToInt(sProductVersion[1]);
      if I <= 5 then
        Result := I - 3
      else
        Result := I;
    end;
  end;
end;

function MyConstructor(aOuter: nsISupports; const aIID: TGUID; out aResult): nsresult; cdecl;
begin

end;

initialization
  mtModule.Version := GetVersion;

  CIDs[0].CID := @Sample_CID;
  CIDs[0].ConstructorProc := @MyConstructor;
  mtModule.CIDs := @CIDs;

  Contracts[0].ContractID := Sample_CONTRACTID;
  Contracts[0].CID := @Sample_CID;
  mtModule.Contracts := @Contracts;

  NSModule := @mtModule;

end.

0

正如您所注意到的,这在FF 5和FF 6中不起作用。相反,您可以添加初始化块以在运行时检查Firefox版本并相应地调整mVersion。 Mozilla有意破坏二进制组件,因此即使在不同版本之间,这也是可行的解决方法。

您可以使用Application.ExeName和http://www.delphitricks.com/source-code/files/get_the_version_of_a_file.html

FF 5 - mVersion:= 2;

FF 6 - mVersion:= 6;

FF 7 - mVersion:= 7;


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接