如何在 COM 类型库中创建模块定义的函数

How do I create a module defined function in a COM Type Library

VBA 使用的 VBE7.dll 类型库具有以下 Conversion 模块的 MIDL:

[
  dllname("VBE7.DLL"),
  uuid(36785f40-2bcc-1069-82d6-00dd010edfaa),
  helpcontext(0x000f6ebe)
]
module Conversion {
    [helpcontext(0x000f6ea2)] 
    BSTR _stdcall _B_str_Hex([in] VARIANT* Number);
    [helpcontext(0x000f652a)] 
    VARIANT _stdcall _B_var_Hex([in] VARIANT* Number);
    [helpcontext(0x000f6ea4)] 
    BSTR _stdcall _B_str_Oct([in] VARIANT* Number);
    [helpcontext(0x000f6557)] 
    VARIANT _stdcall _B_var_Oct([in] VARIANT* Number);
    [hidden, helpcontext(0x000f6859)] 
    long _stdcall MacID([in] BSTR Constant);
    [helpcontext(0x000f6ea9)] 
    BSTR _stdcall _B_str_Str([in] VARIANT* Number);
    [helpcontext(0x000f658a)] 
    VARIANT _stdcall _B_var_Str([in] VARIANT* Number);
    [helpcontext(0x000f659f)] 
    double _stdcall Val([in] BSTR String);
    [helpcontext(0x000f64c8)] 
    BSTR _stdcall CStr([in] VARIANT* Expression);
    [helpcontext(0x000f64c8)] 
    BYTE _stdcall CByte([in] VARIANT* Expression);
    [helpcontext(0x000f64c8)] 
    VARIANT_BOOL _stdcall CBool([in] VARIANT* Expression);
    [helpcontext(0x000f64c8)] 
    CY _stdcall CCur([in] VARIANT* Expression);
    [helpcontext(0x000f64c8)] 
    DATE _stdcall CDate([in] VARIANT* Expression);
    [helpcontext(0x000f6e7a)] 
    VARIANT _stdcall CVDate([in] VARIANT* Expression);
    [helpcontext(0x000f64c8)] 
    short _stdcall CInt([in] VARIANT* Expression);
    [helpcontext(0x000f64c8)] 
    long _stdcall CLng([in] VARIANT* Expression);
    [helpcontext(0x000f64c8)] 
    int64 _stdcall CLngLng([in] VARIANT* Expression);
    [helpcontext(0x000f64c8)] 
    LONG_PTR#i _stdcall CLngPtr([in] VARIANT* Expression);
    [helpcontext(0x000f64c8)] 
    float _stdcall CSng([in] VARIANT* Expression);
    [helpcontext(0x000f64c8)] 
    double _stdcall CDbl([in] VARIANT* Expression);
    [helpcontext(0x000f64c8)] 
    VARIANT _stdcall CVar([in] VARIANT* Expression);
    [helpcontext(0x000f64b5)] 
    VARIANT _stdcall CVErr([in] VARIANT* Expression);
    [helpcontext(0x000f6c6d)] 
    BSTR _stdcall _B_str_Error([in, optional] VARIANT* ErrorNumber);
    [helpcontext(0x000f6c6d)] 
    VARIANT _stdcall _B_var_Error([in, optional] VARIANT* ErrorNumber);
    [helpcontext(0x000f649b)] 
    VARIANT _stdcall Fix([in] VARIANT* Number);
    [helpcontext(0x000f6533)] 
    VARIANT _stdcall Int([in] VARIANT* Number);
    [helpcontext(0x000f64c8)] 
    HRESULT _stdcall CDec(
        [in] VARIANT* Expression,
        [out, retval] VARIANT* pvar
    );
};

我特别感兴趣的是 VBA 如何解释 HRESULT 返回 CDec 函数(上面 MIDL 中的最后一个函数),这样 VBA,CDec 函数的签名为

Function CDec(Expression)

似乎 像 VBA 隐藏了 HRESULT 返回的 TLB 定义,所以为了检验理论,我想创建自己的 TLB在 module 中定义 HRESULT 返回函数,然后查看 VBA 如何处理该函数。

我不相信这可以在 C# 或 VB.NET 中完成,当我尝试在 VB6 的标准模块中定义函数时,该模块在 dll 中不可见。

这可以使用 C++ 实现吗?我需要创建什么样的项目?我需要做什么特别的事情吗?我是否需要手动编辑 MIDL?

注意:我特意没有将此问题标记为 VBA,因为我正在尝试从 C# 解释 TLB。为了测试 VBA 主机如何解释 TLB,我想用任何支持它的语言创建一个合适的 TLB。我可以使用 Visual Studio 6、2003、2013 和 2015。

CDec 声明中重要的是 [out] and [retval] attributes 组合。 理解它的工具(如 VB/VBA)将能够以简化的方式编译对该方法的调用,屏蔽错误处理,因此

HRESULT _stdcall CDec(
        [in] VARIANT* Expression,
        [out, retval] VARIANT* pvar
    );

等同于

VARIANT _stdcall CDec([in] VARIANT* Expression);

equivalent 这里并不意味着它在二进制形式上是等效的,它只是意味着理解语法的工具可以使用(并在最终二进制目标中编译)当他们看到第二个时的第一个表情。 它还意味着如果出现错误(HRESULT 失败),则该工具应该以任何它认为合适的方式引发错误(VB/VBA 会这样做)。

这就是“syntactic sugar”。

您可以使用 MIDL 编写,也可以使用 .NET:只需使用 Visual Studio 创建标准 Class 库并添加此示例 c# class:

[ComVisible(true)]
[ClassInterface(ClassInterfaceType.AutoDual)]
public class Class1
{
    public object Test(object obj)
    {
        return obj;
    }
}

编译它和 运行 注册它的 regasm 工具,使用如下命令:

C:\Windows\Microsoft.NET\Framework64\v4.0.30319\regasm "C:\mypath\ClassLibrary1\bin\Debug\classlibrary1.dll" /tlb /codebase

这会将 class 注册为 COM 对象,并创建一个 C:\mypath\ClassLibrary1\bin\Debug\classlibrary1.tlb 类型库文件。

现在,启动 Excel(您可以使用任何兼容 COM 自动化的客户端),并添加对 ClassLibrary1(开发者模式,VBA 编辑器,Tools /参考)。 如果您没有看到它,您可能 运行 位不同。可以使用 COM 进行 32-64 通信,但现在,只需确保您的客户端 运行 的位数与您的 ClassLibrary1.dll 的编译位数相同。

获得参考后,添加一些 VB 代码,如下所示。

Sub Button1_Click()
    Dim c1 As New Class1
    output = c1.Test("hello from VB")
End Sub

如您所见,VB intellisense 将按照我们预期的方式显示方法,就像在 C# 中一样,并且工作正常。

现在,让我们尝试在 C++ 中使用它:创建一个控制台项目(再次确保位数兼容),然后向其中添加以下代码:

#include "stdafx.h" // needs Windows.h

#import "c:\Windows\Microsoft.NET\Framework64\v4.0.30319\mscorlib.tlb" // adapt to your context
#import "C:\mypath\ClassLibrary1\bin\Debug\classlibrary1.tlb" 

using namespace ClassLibrary1;

int main()
{
  CoInitialize(NULL);

  _Class1Ptr c1(__uuidof(Class1));
  _variant_t output = c1->Test(L"hello from C++");

  wprintf(L"output: %s\n", V_BSTR(&output));

  CoUninitialize();
  return 0;
}

这也可以正常工作,代码看起来接近 VB 的代码。请注意,我使用了 Visual Studio magic #import directive which is super cool because it masks many details of COM Automation 管道(就像 VB/VBA 所做的那样),包括 bstr 和 variant smart classes.

让我们点击 Test 调用并执行 Goto Definition (F12),这是我们看到的:

inline _variant_t _Class1::Test ( const _variant_t & obj ) {
    VARIANT _result;
    VariantInit(&_result);
    HRESULT _hr = raw_Test(obj, &_result);
    if (FAILED(_hr)) _com_issue_errorex(_hr, this, __uuidof(this));
    return _variant_t(_result, false);
}

哈哈!这基本上就是 VB/VBA 也做的卧底。我们可以看到异常处理是如何完成的。同样,如果您在 _Class1Ptr 上按 F12,您将看到以下内容(简化):

_Class1 : IDispatch
{
    // Wrapper methods for error-handling

    ...
    _variant_t Test (
        const _variant_t & obj );
    ...

    // Raw methods provided by interface
    ...
      virtual HRESULT __stdcall raw_Test (
        /*[in]*/ VARIANT obj,
        /*[out,retval]*/ VARIANT * pRetVal ) = 0;

};

到了。如您所见,C# 以二进制形式生成的 Test 方法与预期的一样是 [out, retval] 形式。剩下的都是糖和包装纸。 大多数 COM 接口方法在二进制级别都是使用 [out, retval] 设计的,因为编译器不支持函数 return.

的通用兼容二进制格式

VBE7 定义的是 dispinterface,又是某种形式的语法糖,用于在 COM raw/binary IUnknown 接口之上定义接口。 唯一的谜团是为什么 CDec 的定义与 VBE7 中的其他方法不同。我没有答案。

现在,特别是关于 IDL 中的 module 关键字,IDL 只是一个抽象定义(函数、常量、classes 等)工具,可以选择性地输出工件(.H、. C、.TLB 等)针对特定语言(C/C++ 等)或针对特定客户。

正好VB/VBA支持TLB的常量和方法。它将常量解释为它们的本来面目,并将模块中的函数解释为从模块的 dll 名称导出的 DLL。

因此,如果您在磁盘上的某处创建此 my.idl 文件:

[
    uuid(00001234-0001-0000-0000-012345678901)
]
library MyLib
{   
    [
        uuid(00001234-0002-0000-0000-012345678901),
        dllname("kernel32.dll")
    ]
    module MyModule
    {
        const int MyConst = 1234;

        // note this is the real GetCurrentThreadId from kernel32.dll
        [entry("GetCurrentThreadId")]
        int GetCurrentThreadId();
    }
}

你可以像这样从中编译一个 TLB:

midl c:\mypath\my.idl /out c:\mypath

它将创建一个 my.tlb 文件,您可以在 VB/VBA 中引用该文件。现在从 VB/VBA 开始,您有一个可用的新函数(智能感知将在其上运行),称为 GetCurrentThreadId。它之所以有效,是因为 Windows' kernel32.dll 确实导出了一个 GetCurrentThreadId 函数。

您只能从 C/C++ 项目(以及其他 languages/tools,如 Delphi)创建 DLL Exports,但不能从 VB/VBA,不能来自 .NET。

事实上,在 .NET 中创建导出有一些技巧,但不是真正标准:Is is possible to export functions from a C# DLL like in VS C++?