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

时间:2017-06-13 00:32:22

标签: c++ module typelib hresult midl

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来定义{{1} }返回HRESULT内的函数,然后查看VBA如何处理该函数。

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

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

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

1 个答案:

答案 0 :(得分:6)

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

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

等同于

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

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

那只是“syntactic sugar”。

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

[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

这会将该类注册为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

正如您将体验到的那样,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这非常酷,因为它掩盖了COM Automation管道的许多细节(就像VB / VBA一样),包括bstr和变体智能类。

让我们点击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);
}

哈哈!这基本上是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]设计,因为编译器不支持函数返回的通用兼容二进制格式。

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

现在,特别是关于IDL中的module关键字,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();
    }
}

您可以像这样编译TLB:

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

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

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

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