为什么在互操作中不能使用WideString作为函数返回值?

49

我曾多次建议人们在互操作方面使用返回值类型为WideString

想法是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时出现访问冲突。

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

[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 函数返回值是可行的 BSTR 呢?还是这只是 Delphi 的缺陷?


6
David,也许可以加上“C++”和“C#”标签吗? - kobik
@J...我从来没见过不返回HRESULT的COM方法。虽然我说的不是在COM中使用BSTR,而是把它作为在不同模块之间共享堆的一种便捷方式。 - David Heffernan
@David:我已经说过了:C语言没有统一的方法来返回非POD类型。有时像Delphi一样作为引用参数,有时在一个甚至两个寄存器中,有时在某种堆栈上等等。其他语言也可能使用其中之一。而BSTR指向的类型还有一个前导长度dword。这就是为什么它不能像普通的PWideChar一样处理,就像AnsiString或UnicodeString一样,即使它们实际上是这样的。 - Rudy Velthuis
@rudy BSTR是POD,所以你的评论在这里不适用。我可以毫不费力地返回TBStr。这只是为了参数传递而使用的PWideChar。 - David Heffernan
1
@DavidHeffernan,所以 procedure TestWideStringOutParam(var str: WideString); stdcall(注意 var)不能工作吗?还是我理解错了?(因为它确实可以工作) - kobik
显示剩余16条评论
2个回答

26

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

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;

6
听起来很有道理,但是Pointer(result) := nil本身会触发访问冲突错误(AV)。 - David Heffernan
对于函数,Delphi将结果指针存储在EAX中。这基本上解释了它。从Delphi的角度来看,您无法将“没有变量”作为var参数传入。 - Sean B. Durkin
6
Pointer(Result) := nil 引发了 AV 错误,因为实际上返回类型是指向 WideString 的指针(隐藏的输出参数)。通过将其赋值为 nil,指针(从未由 C++ 处理)被间接引用:mov eax,[ebp+$08]; xor edx,edx; mov [eax],edx。换句话说:WideString 返回值始终作为隐藏的输出参数传递。Delphi 不允许更改该行为。 - Andreas Hausladen
7
然而,通过返回一个PWideChar指针可能会欺骗 Delphi:(未经测试) function TestWideString: PWideChar; stdcall; var RealResult: WideString absolute Result; begin Initialize(RealResult); RealResult := 'TestWideString'; end; - user743382
1
@DavidHeffernan 您说得对,那部分确实是一个不好的论点,但我仍然坚持我的结论。WideStringBSTR都具有指针大小,但这并不意味着它们总是以相同的方式传递。它们足够接近,因此在过程和函数参数中以相同的方式传递,但如果stdcall调用约定通过隐藏的out参数返回结构,并且将WideString视为结构,则它将不会像BSTRPWideChar)一样返回。 - user743382
显示剩余11条评论

19
在C#/ C ++中,您需要将结果定义为out参数,以保持stdcall调用约定的二进制代码兼容性: 从DLL函数返回字符串和接口引用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);

5
+1 是的,就是这样。但我仍然无法理解这里实际发生了什么!! - David Heffernan
请注意,根据我的测试结果,如果您有多个参数,则“Result”参数似乎总是在列表中的第一个,而不是最后一个,这可能与您的预期不同。 - Jamie Kitson
@JamieKitson 我不明白那个评论的意思。如果你指的是Delphi中用于返回函数返回值的隐式var参数,那么额外的参数会在其他参数之后传递。这在这里有清晰的文档说明:http://docwiki.embarcadero.com/RADStudio/en/Program_Control#Handling_Function_Results - David Heffernan
@DavidHeffernan 或许Jamie所观察到的是,使用stdcall时参数传递的顺序是相反的,正如你提供链接中所述(先传后面的参数)。因此,在声明/Delphi端最后的"result"参数在stub/asm级别上被传递为第一个。 - Arnaud Bouchez

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