什么是safecall?

16

我正在使用VB6创建ActiveX EXE,但是我所得到的唯一示例都是用Delphi编写的。

阅读示例代码时,我注意到有一些函数在其签名后面跟着safecall关键字。以下是一个示例:

function AddSymbol(ASymbol: OleVariant): WordBool; safecall;

这个关键字的目的是什么?


1
http://en.wikipedia.org/wiki/X86_calling_conventions#safecall - Greg Hewgill
4个回答

14

Safecall将参数从右往左传递,而不是像Pascal或寄存器(默认)那样从左到右传递。

使用safecall时,过程或函数在返回时将参数从堆栈中删除(类似于Pascal,但不像Cdecl那样由调用方负责)。

Safecall实现了异常“防火墙”;特别是在Win32上,这实现了进程间COM错误通知。否则,它与stdcall相同(win api使用的另一种调用约定)。


14
此外,异常防火墙通过使用支持IErrorInfo的对象调用SetErrorInfo()来工作,以便调用者可以获得有关异常的扩展信息。这是TComObject和TAutoIntfObject中TObject.SafeCallException重写的功能。这两种类型还实现了ISupportErrorInfo来标记这一事实。
在发生异常时,safecall方法的调用者可以查询ISupportErrorInfo,然后查询该接口的失败HRESULT(高位设置)所导致的方法,并且如果返回S_OK,GetErrorInfo()可以获得异常信息(描述、帮助等),形式为Delphi RTL在SafeCallException重写中传递给SetErrorInfo()的IErrorInfo实现。

4
在COM中,每个方法都是一个返回HRESULT的函数:
IThingy = interface
   ['{357D8D61-0504-446F-BE13-4A3BBE699B05}']
   function AddSymbol(ASymbol: OleVariant; out RetValue: WordBool): HRESULT; stdcall;
end;

COM中有一个绝对的规则:

  • 在COM中没有异常
  • 每一项操作都返回一个HRESULT
  • 负数的HRESULT表示操作失败
  • 在高级语言中,失败会被映射为异常

COM设计者的意图是让高级语言自动将Failed方法转换成异常。

因此,在您自己的语言中,COM调用将被表示为不带HRESULT的形式。例如:

  • 类似Delphi: function AddSymbol(ASymbol: OleVariant): WordBool;
  • 类似C#: WordBool AddSymbol(OleVariant ASymbol);

在Delphi中,您可以选择使用原始的函数签名:

IThingy = interface
   ['{357D8D61-0504-446F-BE13-4A3BBE699B05}']
   function AddSymbol(ASymbol: OleVariant; out RetValue: WordBool): HRESULT; stdcall;
end;

而且需要自己处理异常:

bAdded: WordBool;
thingy: IThingy;
hr: HRESULT;

hr := thingy.AddSymbol('Seven', {out}bAdded);
if Failed(hr) then
    OleError(hr);

或者使用更简短的等效语句:
bAdded: WordBool;
thingy: IThingy;
hr: HRESULT;

hr := thingy.AddSymbol('Seven', {out}bAdded);
OleCheck(hr);

或者使用更短的等效写法:
bAdded: WordBool;
thingy: IThingy;

OleCheck(thingy.AddSymbol('Seven'), {out}bAdded);

COM并不希望你处理HRESULTs

但是你可以要求Delphi将这些细节隐藏起来,这样你就可以专注于编程:

IThingy = interface
   ['{357D8D61-0504-446F-BE13-4A3BBE699B05}']
   function AddSymbol(ASymbol: OleVariant): WordBool; safecall;
end;

在幕后,编译器仍会检查返回的 HRESULT 值,如果 HRESULT 表示失败(即为负数),则会抛出 EOleSysError 异常。由编译器生成的 safecall 版本在功能上与以下代码等效:

function AddSymbol(ASymbol: OleVariant): WordBool; safecall;
var
   hr: HRESULT;
begin
   hr := AddSymbol(ASymbol, {out}Result);
   OleCheck(hr);
end;

但这使您可以简单地调用:
bAdded: WordBool;
thingy: IThingy;

bAdded := thingy.AddSymbol('Seven');

简而言之:您可以使用以下任一选项:

function AddSymbol(ASymbol: OleVariant; out RetValue: WordBool): HRESULT; stdcall;
function AddSymbol(ASymbol: OleVariant): WordBool; safecall;

但前者要求您每次都处理HRESULT。

额外闲话

您几乎从不希望自己处理HRESULT;它会使程序混杂着毫无意义的噪音。但是有时您可能想要检查HRESULT自己(例如,您想处理一个不太异常的故障)。最新版本的Delphi已经开始包括翻译过的Windows头文件接口,这些接口声明为双向:

IThingy = interface
   ['{357D8D61-0504-446F-BE13-4A3BBE699B05}']
   function AddSymbol(ASymbol: OleVariant; out RetValue: WordBool): HRESULT; stdcall;
end;

IThingySC = interface
   ['{357D8D61-0504-446F-BE13-4A3BBE699B05}']
   function AddSymbol(ASymbol: OleVariant): WordBool); safecall;
end;

或从RTL源代码中:

  ITransaction = interface(IUnknown)
    ['{0FB15084-AF41-11CE-BD2B-204C4F4F5020}']
    function Commit(fRetaining: BOOL; grfTC: UINT; grfRM: UINT): HResult; stdcall;
    function Abort(pboidReason: PBOID; fRetaining: BOOL; fAsync: BOOL): HResult; stdcall;
    function GetTransactionInfo(out pinfo: XACTTRANSINFO): HResult; stdcall;
  end;

  { Safecall Version }
  ITransactionSC = interface(IUnknown)
    ['{0FB15084-AF41-11CE-BD2B-204C4F4F5020}']
    procedure Commit(fRetaining: BOOL; grfTC: UINT; grfRM: UINT); safecall;
    procedure Abort(pboidReason: PBOID; fRetaining: BOOL; fAsync: BOOL); safecall;
    procedure GetTransactionInfo(out pinfo: XACTTRANSINFO); safecall;
  end;

SC后缀代表safecall。两个接口是等效的,您可以根据需要选择将COM变量声明为哪个接口:

//thingy: IThingy;
thingy: IThingySC;

你甚至可以在它们之间进行转换:
thingy: IThingSC;
bAdded: WordBool;

thingy := CreateOleObject('Supercool.Thingy') as TThingySC;

if Failed(IThingy(thingy).AddSymbol('Seven', {out}bAdded) then
begin
   //Couldn't seven? No sixty-nine for you
   thingy.SubtractSymbol('Sixty-nine');
end;

额外奖励 - C#

C#默认情况下执行与Delphi的safecall相等操作,不同之处在于:

  • 你需要选择退出safecall映射,而不是选择加入

在C#中,你可以将COM接口声明为:

[ComImport]
[Guid("{357D8D61-0504-446F-BE13-4A3BBE699B05}")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IThingy
{
   WordBool AddSymbol(OleVariant ASymbol);
   WordBool SubtractSymbol(OleVariant ASymbol);
}

你会发现COM的HRESULT被隐藏了。C#编译器和Delphi编译器一样,将自动检查返回的HRESULT并为你抛出异常。
在C#中,就像在Delphi中一样,你可以选择自己处理HRESULTs:
[ComImport]
[Guid("{357D8D61-0504-446F-BE13-4A3BBE699B05}")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IThingy
{
   [PreserveSig]
   HRESULT AddSymbol(OleVariant ASymbol, out WordBool RetValue);

   WordBool SubtractSymbol(OleVariant ASymbol);
}
[PreserveSig]是告诉编译器要保留方法签名的精确格式。它用于指示未托管的方法具有HRESULTretval返回值时,这些返回值是否直接转换,还是自动转换为异常。

3

弗朗索瓦所说的,如果没有safecall,你的COM方法调用将会像下面这样,并且你需要自己进行错误检查而不是获取异常。

function AddSymbol(ASymbol: OleVariant; out Result: WordBool): HResult; stdcall;

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