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

Thu*_*ame 8 c++ module typelib midl hresult

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

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

Function CDec(Expression)
Run Code Online (Sandbox Code Playgroud)

它看起来像VBA是阴影的HRESULT返回TLB定义,这样来检验这个理论,我想创建自己的TLB定义了一个HRESULT内部的返回功能module,然后看VBA如何处理该功能.

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

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

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

Sim*_*ier 6

CDec声明中重要的是[out]和[retval]属性的组合.理解它的工具(比如VB/VBA)将能够以简化的方式编译对该方法的调用,掩盖错误处理,所以

HRESULT _stdcall CDec(
        [in] VARIANT* Expression,
        [out, retval] VARIANT* pvar
    );
Run Code Online (Sandbox Code Playgroud)

是等同于

VARIANT _stdcall CDec([in] VARIANT* Expression);
Run Code Online (Sandbox Code Playgroud)

等效于此并不意味着它在二进制形式中是等价的,它只是意味着理解语法的工具可以在第一个表达式看到第二个时使用(并在最终的二进制目标中编译).它还意味着如果出现错误(HRESULT失败),那么工具应该以它认为合适的任何方式引发错误(VB/VBA将执行此操作).

这只是" 语法糖 ".

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

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

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

C:\Windows\Microsoft.NET\Framework64\v4.0.30319\regasm "C:\mypath\ClassLibrary1\bin\Debug\classlibrary1.dll" /tlb /codebase
Run Code Online (Sandbox Code Playgroud)

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

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

获得引用后,添加一些VB代码,如下所示.

Sub Button1_Click()
    Dim c1 As New Class1
    output = c1.Test("hello from VB")
End Sub
Run Code Online (Sandbox Code Playgroud)

正如您将体验的那样,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;
}
Run Code Online (Sandbox Code Playgroud)

这也可以正常工作,代码看起来很接近VB的代码.注意我使用了Visual Studio magic #import指令 ,它非常酷,因为它掩盖了COM Automation管道的许多细节(就像VB/VBA一样),包括bstr和variant smart类.

让我们点击Test通话并进行转到定义(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);
}
Run Code Online (Sandbox Code Playgroud)

哈哈!这基本上是VB/VBA所做的内容.我们可以看到异常处理是如何完成的.同样,如果你打开F12 _Class1Ptr,这就是你会看到的(简化):

_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;

};
Run Code Online (Sandbox Code Playgroud)

我们到了.如您所见,TestC#以二进制形式生成的方法[out, retval]具有预期的形式.其余的都是糖和包装纸.大多数COM接口方法在二进制级别使用[out,retval]设计,因为编译器不支持函数返回的通用兼容二进制格式.

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

现在,特别是关于moduleIDL中的关键字,IDL只是一个抽象定义(函数,常量,类等)工具,可选择输出针对特定语言的人工制品(.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();
    }
}
Run Code Online (Sandbox Code Playgroud)

您可以像这样编译TLB:

midl c:\mypath\my.idl /out c:\mypath
Run Code Online (Sandbox Code Playgroud)

它将创建一个my.tlb可以在VB/VBA中引用的文件.现在从VB/VBA,您可以使用一个新函数(intellisense将对其进行处理)GetCurrentThreadId.它的工作原理是因为Windows的kernel32.dll确实导出了GetCurrentThreadId函数.

您只能从C/C++项目(以及其他语言/工具,如Delphi)创建DLL导出,但不能从VB/VBA 创建DLL导出,而不能从.NET创建.

事实上,在.NET中创建导出有一些技巧,但它并不是真正的标准:是否可以从VS C++中的C#DLL导出函数?