为啥不能将 WideString 用作互操作的函数返回值?

Posted

技术标签:

【中文标题】为啥不能将 WideString 用作互操作的函数返回值?【英文标题】:Why can a WideString not be used as a function return value for interop?为什么不能将 WideString 用作互操作的函数返回值? 【发布时间】:2012-03-10 02:36:51 【问题描述】:

我曾不止一次建议人们将WideString 类型的返回值用于互操作目的。

Accessing Delphi DLL throwing ocasional exception ASP.NET web app calling Delphi DLL on IIS webserver, locks up when returning PChar string Why can Delphi DLLs use WideString without using ShareMem?

这个想法是WideStringBSTR 相同。因为BSTR 是在共享COM 堆上分配的,所以在一个模块中分配并在另一个模块中释放是没有问题的。这是因为各方都同意使用同一个堆,即 COM 堆。

但是,WideString 似乎不能用作互操作的函数返回值。

考虑以下 Delphi DLL。

library WideStringTest;

uses
  ActiveX;

function TestWideString: WideString; stdcall;
begin
  Result := 'TestWideString';
end;

function TestBSTR: TBstr; stdcall;
begin
  Result := SysAllocString('TestBSTR');
end;

procedure TestWideStringOutParam(out str: WideString); stdcall;
begin
  str := 'TestWideStringOutParam';
end;

exports
  TestWideString, TestBSTR, TestWideStringOutParam;

begin
end.

以及以下 C++ 代码:

typedef BSTR (__stdcall *Func)();
typedef void (__stdcall *OutParam)(BSTR &pstr);

HMODULE lib = LoadLibrary(DLLNAME);
Func TestWideString = (Func) GetProcAddress(lib, "TestWideString");
Func TestBSTR = (Func) GetProcAddress(lib, "TestBSTR");
OutParam TestWideStringOutParam = (OutParam) GetProcAddress(lib,
                   "TestWideStringOutParam");

BSTR str = TestBSTR();
wprintf(L"%s\n", str);
SysFreeString(str);
str = NULL;

TestWideStringOutParam(str);
wprintf(L"%s\n", str);
SysFreeString(str);
str = NULL;

str = TestWideString();//fails here
wprintf(L"%s\n", str);
SysFreeString(str);

TestWideString 的调用失败并出现以下错误:

BSTRtest.exe 中 0x772015de 处未处理的异常:0xC0000005:访问冲突读取位置 0x00000000。

同样,如果我们尝试从 C# 中使用 p/invoke 调用它,我们会失败:

[DllImport(@"path\to\my\dll")]
[return: MarshalAs(UnmanagedType.BStr)]
static extern string TestWideString();

错误是:

ConsoleApplication10.exe 中出现“System.Runtime.InteropServices.SEHException”类型的未处理异常

附加信息:外部组件已抛出异常。

通过 p/invoke 调用 TestWideString 可以正常工作。

因此,对 WideString 参数使用传递引用并将它们映射到 BSTR 似乎工作得很好。但不适用于函数返回值。我在 Delphi 5、2010 和 XE2 上对此进行了测试,并在所有版本上观察到相同的行为。

执行进入 Delphi 并几乎立即失败。对Result 的赋值变成了对System._WStrAsg 的调用,其第一行内容如下:

CMP [EAX],EDX

现在,EAX$00000000,自然存在访问冲突。

谁能解释一下?难道我做错了什么?我期望WideString 函数值是可行的BSTRs 是不合理的吗?还是只是 Delphi 的缺陷?

【问题讨论】:

大卫,也许还添加C++C# 标签? @kobik 我相信这确实是一个关于Delphi如何实现返回值的问题。我认为 Delphi 是一个奇怪的问题。 @J... 我从未见过不返回 HRESULT 的 COM 方法。我不是在谈论在 COM 中使用 BSTR。我说它是一种在不同模块之间共享堆的便捷方式。 @J... 分配给 WideString,它确实调用了 SysAllocString。或者它可能是 SysReallocString,但这在道德上是等价的。 @DavidHeffernan,所以procedure TestWideStringOutParam(var str: WideString); stdcall(注意var)不起作用?还是我仍然弄错了? (因为它确实有效) 【参考方案1】:

在常规的 Delphi 函数中,函数返回实际上是一个通过引用传递的参数,尽管在语法上它看起来和感觉就像一个“输出”参数。您可以像这样测试它(这可能取决于版本):

function DoNothing: IInterface;
begin
  if Assigned(Result) then
    ShowMessage('result assigned before invocation')
  else
    ShowMessage('result NOT assigned before invocation');
end;

procedure TestParameterPassingMechanismOfFunctions;
var
  X: IInterface;
begin
  X := TInterfaceObject.Create;
  X := DoNothing; 
end;

演示调用TestParameterPassingMechanismOfFunctions()

您的代码失败是因为 Delphi 和 C++ 对与函数结果的传递机制相关的调用约定的理解不匹配。在 C++ 中,函数返回的行为类似于语法所示:out 参数。但对于 Delphi,它是一个 var 参数。

要修复,试试这个:

function TestWideString: WideString; stdcall;
begin
  Pointer(Result) := nil;
  Result := 'TestWideString';
end;

【讨论】:

这听起来有道理,但Pointer(result) := nil 本身就引发了 AV。 对于函数,Delphi 将指向结果的指针存储在 EAX 中。这几乎解释了它。从 Delphi 的角度来看,您不能将“无变量”作为 var 参数传递。 Pointer(Result) := nil 会抛出 AV,因为返回类型实际上是指向 WideString 的指针(隐藏参数)。通过将其分配为零,指针(从未被 C++ 处理过)被推迟:mov eax,[ebp+$08]; xor edx,edx; mov [eax],edx。换句话说:WideString 返回值总是作为隐藏参数传递。 Delphi 不允许你改变这种行为。 然而,有可能通过返回PWideChar: (未经测试)function TestWideString: PWideChar; stdcall; var RealResult: WideString absolute Result; begin Initialize(RealResult); RealResult := 'TestWideString'; end;来欺骗Delphi @DavidHeffernan 你说得对,那部分是一个糟糕的论点,但我坚持我的结论。 WideStringBSTR 都具有指针的大小,但这并不意味着它们总是以相同的方式传递。它们足够接近,因此它们以相同的方式传递给过程和函数参数,但是如果stdcall 调用约定通过隐藏的out 参数返回结构,并且WideString 被视为结构,那么它赢了'返回的方式与 BSTR (PWideChar) 不同。【参考方案2】:

在 C#/C++ 中,您需要将 Result 定义为 out 参数,以保持 stdcall 调用约定的二进制代码兼容性:

Returning Strings and Interface References From DLL Functions

stdcall 调用约定中,函数的结果通过CPU 的EAX 寄存器传递。但是,Visual C++ 和 Delphi 为这些例程生成不同的二进制代码。

Delphi 代码保持不变:

function TestWideString: WideString; stdcall;
begin
  Result := 'TestWideString';
end;

C#代码:

// declaration
[DllImport(@"Test.dll")]        
static extern void  TestWideString([MarshalAs(UnmanagedType.BStr)] out string Result);
...
string s;
TestWideString(out s); 
MessageBox.Show(s);

【讨论】:

+1 是的。不过,我仍然无法理解这里到底发生了什么! 请注意,从我的测试来看,如果您有多个参数,Result 参数似乎总是列表中的第一个,而不是可能假设的最后一个。 @JamieKitson 我不明白那个评论。如果您的意思是用于返回函数返回值的 Delphi 隐式 var 参数,则额外的参数将在其他参数之后传递。它在这里清楚地记录:docwiki.embarcadero.com/RADStudio/en/… @DavidHeffernan 也许 Jamie 观察到的是参数以与stdcall 相反的顺序传递,正如您的链接也指出的那样(最后一个)。所以“result”参数是声明/Delphi 端的最后一个参数,在存根/asm 级别首先传递。

以上是关于为啥不能将 WideString 用作互操作的函数返回值?的主要内容,如果未能解决你的问题,请参考以下文章

如何在vb6中注册一个不能用作com互操作的.net DLL?

为啥成员函数不能用作模板参数?

为啥表达式'I_VALUE'不能用作函数中的赋值目标

为啥我们不能使用 CloudFormation 中的参数将 AllowedValues 用作字符串?

为啥这个互操作会使 .NET 运行时崩溃?

为啥 ref 结构不能用作类型参数?