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

Dav*_*nan 48 delphi

我不止一次建议人们使用类型的返回值WideString进行互操作.

这个想法是a WideString和a一样BSTR.因为a 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.
Run Code Online (Sandbox Code Playgroud)

和以下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);
Run Code Online (Sandbox Code Playgroud)

调用TestWideString失败并显示以下错误:

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

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

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

错误是:

ConsoleApplication10.exe中发生未处理的"System.Runtime.InteropServices.SEHException"类型异常

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

调用TestWideString通过P/Invoke的作品如预期.

因此,使用带有WideString参数的pass-by-reference并将它们映射到BSTR看起来非常好.但不是函数返回值.我在Delphi 5,2010和XE2上测试了这一点,并在所有版本上观察到相同的行为.

执行进入Delphi并几乎立即失败.Result转换为调用的赋值System._WStrAsg,其第一行读取:

CMP     [EAX],EDX

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

有谁能解释一下?难道我做错了什么?我期望WideString函数值是可行BSTR的,这是不合理的吗?或者它只是一个Delphi缺陷?

Sea*_*kin 25

在常规的Delphi函数中,函数return实际上是一个通过引用传递的参数,即使在语法上它看起来和感觉就像一个'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;
Run Code Online (Sandbox Code Playgroud)

演示电话 TestParameterPassingMechanismOfFunctions()

您的代码失败是因为Delphi和C++对函数结果传递机制的调用约定的理解不匹配.在C++中,函数返回的行为类似于语法建议:out参数.但对于Delphi来说,这是一个var参数.

要修复,请尝试以下方法:

function TestWideString: WideString; stdcall;
begin
  Pointer(Result) := nil;
  Result := 'TestWideString';
end;
Run Code Online (Sandbox Code Playgroud)

  • 但是,有可能通过返回`PWideChar` :(未经测试)`函数TestWideString:PWideChar来欺骗Delphi; STDCALL; var RealResult:WideString absolute Result; begin Initialize(RealResult); RealResult:='TestWideString'; 结束;` (6认同)
  • 这听起来似乎有道理,但是`指针(结果):= nil`本身会引发AV. (5认同)
  • `Pointer(Result):= nil`抛出AV,因为返回类型实际上是指向WideString(隐藏的参数)的指针.并且通过赋值为零,指针(从未被C++处理过)被引用:`mov eax,[ebp + $ 08]; xor edx,edx; mov [eax],edx`.换句话说:WideString返回值始终作为隐藏输出参数传递.Delphi不允许您更改该行为. (5认同)

kob*_*bik 19

在C#/ C++中,您需要将Result定义为outParameter,以便维护stdcall调用约定的二进制代码兼容性:

从DLL函数返回字符串和接口引用

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

Delphi代码保持不变:

function TestWideString: WideString; stdcall;
begin
  Result := 'TestWideString';
end;
Run Code Online (Sandbox Code Playgroud)

C#代码:

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

  • +1是的,这样做.我仍然无法理解这里真正发生的事情!! (5认同)