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

8

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如何解释返回CDec函数(MIDL中最后一个函数)的HRESULT,从而使得在VBA内部,CDec函数具有下列签名:

Function CDec(Expression)

似乎VBA正在隐藏返回HRESULT的TLB定义,为了测试这个理论,我想创建自己的TLB,在一个模块中定义一个返回HRESULT的函数,然后看看VBA如何处理该函数。
我不认为C#或VB.NET可以做到这一点,在VB6中尝试在标准模块中定义函数时,该模块在dll中不可见。
使用C++是否可能实现这一点?我需要创建哪种类型的项目?有什么特别之处需要注意吗?我需要手动编辑MIDL吗?
注意:我特意没有将此问题标记为VBA,因为我正在尝试从C#解释TLB。为了测试VBA主机如何解释TLB,我想在支持它的任何语言中创建一个合适的TLB。我可以使用Visual Studio 6、2003、2013和2015。

我会假设当返回的 HRESULT 值表示错误时,VBA 会断言某种异常。 - Ross Ridge
VBA不会将函数公开为返回HRESULT并接受2个参数。它只允许返回Variant并接受单个参数。我正在尝试确定当解释TLB时,VBA是否特殊处理CDec函数,或者它是否以相同方式处理所有类似定义的函数。 - ThunderFrame
我告诉过你,我认为它特殊处理了这个函数。 - Ross Ridge
请注意,CDec "123"(无返回值分配)是有效的,而Fix "123"则无法编译 - 奇怪!此外,Fix可能会在无效输入时引发错误,尽管没有HRESULT在视线中。此外,VARIANT是一个巨大的结构体,用于返回值,因此C/C++编译器会隐式地为其创建输出参数,该参数位于参数列表中的第一位。 - wqw
这可能完全偏题,但你的源代码是Python开发者的噩梦。 - user5870134
显示剩余2条评论
1个回答

6
重要的是在 CDec 声明中,结合了 [out] 和 [retval] 属性。理解这些工具(如 VB/VBA)将能够以简化的方式编译对该方法的调用,掩盖错误处理。请参考[out] 和 [retval] 属性
HRESULT _stdcall CDec(
        [in] VARIANT* Expression,
        [out, retval] VARIANT* pvar
    );

等同于的意思

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

这里的“等效”并不是二进制形式上的等同,而只是指理解该语法的工具可以使用第一种表达式(并在生成的最终二进制目标中编译),当它们看到第二个表达式时。这也意味着如果出现错误(HRESULT 失败),则该工具应以任何适当的方式引发错误(VB/VBA 将执行此操作)。
这就是所谓的“语法糖”。
您可以使用 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的魔法#import指令,这非常酷,因为它掩盖了许多COM自动化管道的细节(就像VB/VBA那样),包括bstr和variant智能类。

让我们点击“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]进行设计,因为编译器不支持函数返回的通用兼容二进制格式。

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

现在,具体来说,IDL中的module关键字是什么,IDL只是一个抽象的定义(函数、常量、类等)工具,可选择输出针对特定语言(C/C++等)或特定客户端的工件(.H、.C、.TLB等)。

恰巧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++项目(以及其他语言/工具如Delphi)中创建DLL导出函数,但不能从VB/VBA、.NET中创建。
实际上,有一些技巧可以在.NET中创建导出函数,但这并不是标准的:是否可以像VS C++那样从C# DLL中导出函数?

谢谢。我已经了解了CoClass和Interface中的行为。例如,我可以看到_ErrObject接口具有返回HRESULT的签名,但是CoClass“将它们包装起来”。我的问题特别围绕如何创建模块中的签名(而不是Interfaces/CoClasses),以便我可以测试VBA如何处理它们。最终,我试图发现VBA是否特殊处理CDec或者它是否以相同的方式处理所有模块定义的返回HRESULT的签名,但是由于我找不到包含模块定义函数的其他TLB,所以我想自己编写一个。 - ThunderFrame
@ThunderFrame - 你混淆了很多概念。CoClass并不包装任何东西,它只是一个定义。模块定义函数,其行为与接口方法相同(也是函数)。TLB仅包含定义(元数据、类、方法),而不包含代码。你可以使用MIDL自己制作TLB,但那么你会用它做什么呢?你的问题远非清晰明了。 - Simon Mourier
可能是这样,但MIDL明显区分了在类中定义的函数和在“模块”中定义的函数。我的问题特别是关于如何在模块中创建一个函数而不是在类中创建,但我所知道的是,一个在模块中定义的函数可能需要在类中定义,但需要装饰以使其在MIDL中显示为模块? - ThunderFrame
@ThunderFrame - 我已经更新了,但我仍然不确定你想要什么 :-) - Simon Mourier

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