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

时间:2021-08-04 15:34:43

The VBE7.dll type library used by VBA, has the following MIDL for the Conversion module:

VBE7。VBA使用的dll类型库,具有以下的转换模块的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
    );
};

Where I'm particularly interested in how VBA interprets the HRESULT returning CDec function (the last function in the MIDL above), such that within VBA, the CDec function has a signature of

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

Function CDec(Expression)

It seems like VBA is shadowing the HRESULT returning TLB definition, so to test the theory, I'd like to create my own TLB that defines an HRESULT returning function within a module, and then see how VBA treats that function.

看起来VBA正在对HRESULT返回TLB定义进行跟踪,所以为了测试这个理论,我想创建一个自己的TLB,它在一个模块中定义一个HRESULT返回函数,然后看看VBA是如何处理这个函数的。

I don't believe this can be done in C# or VB.NET, and when I tried defining a function in a standard module in VB6, the module wasn't visible in the dll.

我不相信c#或VB可以做到这一点。NET,当我尝试在VB6的标准模块中定义一个函数时,该模块在dll中不可见。

Is this possible using C++? What sort of project do I need to create? Is there anything special that I need to do? Do I perhaps need to edit the MIDL by hand?

使用c++可以实现吗?我需要创建什么样的项目?我有什么特别需要做的吗?我可能需要手工编辑MIDL吗?

Note: I'm specifically not tagging this question as VBA, as I'm trying to interpret a TLB from C#. In order to test how the VBA host interprets a TLB, I'd like to create an appropriate TLB in any language that supports it. I have Visual Studio 6, 2003, 2013 and 2015 at my disposal.

注意:我并没有把这个问题标记为VBA,因为我正在尝试从c#解释一个TLB。为了测试VBA主机如何解释TLB,我希望在任何支持TLB的语言中创建适当的TLB。我有Visual Studio 6, 2003, 2013和2015供我使用。

1 个解决方案

#1


5  

What's important in CDec declaration is the [out] and [retval] attributes combined. Tools that understand it (like VB/VBA) will be capable of compiling calls to this method in a simplified way, masking error handling, so

CDec声明中重要的是[out]和[retval]属性的组合。理解它的工具(如VB/VBA)能够以一种简化的方式编译对该方法的调用,从而屏蔽错误处理

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

is equivalent to

相当于

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

equivalent here does not mean it's equivalent in binary form, it just means the tools that understand that syntax will be ok to use (and compile in the final binary target) the first expression when they see the second. It also implies that if there is an error (HRESULT failure) then the tool should raise an error by any way it sees fit (VB/VBA will do this).

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

That's simply "syntactic sugar".

这是简单的“语法”。

You can write that using MIDL, but also .NET: just create a standard Class Library using Visual Studio and add this sample c# class:

您可以使用MIDL,但也可以使用。net:使用Visual Studio创建一个标准类库,并添加这个c#类示例:

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

Compile that and run the regasm tool to register it, with a command like this:

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

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

This will register the class as a COM object, and create a C:\mypath\ClassLibrary1\bin\Debug\classlibrary1.tlb type library file.

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

Now, start Excel (you could use any COM automation compatible client), and add a reference to the ClassLibrary1 (developer mode, VBA editor, Tools / Reference). If you don't see it you may be running with a different bitness. It's possible to use COM for 32-64 communication, but for now, just make sure your client runs at the same bitness as how your ClassLibrary1.dll was compiled.

现在,启动Excel(您可以使用任何与COM自动化兼容的客户端),并向ClassLibrary1添加一个引用(开发人员模式、VBA编辑器、工具/引用)。如果你没有看到它,你可能会以不同的方式运行。使用COM进行32-64通信是可能的,但就目前而言,只需确保您的客户端与ClassLibrary1的运行方式相同。dll被编译。

Once you have the reference, add some VB code, like this.

一旦有了引用,就添加一些VB代码,像这样。

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

As you will experience, VB intellisense will show the method like we expect, like in C#, and it works fine.

正如您将看到的,VB intellisense将会像我们所期望的那样显示这个方法,就像c#一样,并且它工作得很好。

Now, let's try to use it from C++: create a console project (again, make sure the bitness is compatible), and add this code to it:

现在,让我们尝试从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;
}

This will also work fine and the code looks close to VB's one. Notice I used Visual Studio magic #import directive which is super cool because it masks many details of COM Automation plumbing (just like VB/VBA does), including bstr and variant smart classes.

这也会很好地工作,并且代码看起来很接近VB的代码。注意,我使用了Visual Studio magic #import指令,它非常酷,因为它掩盖了COM自动化管道的许多细节(就像VB/VBA那样),包括bstr和变体智能类。

Let's click on the Test call and do a Goto Definition (F12), this is what we see:

让我们点击测试调用并做一个Goto定义(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);
}

haha! This is basically what VB/VBA does also undercovers. We can see how exception handling is done. Again, if you do an F12 on _Class1Ptr, this is what you'll see (simplified):

哈哈!这基本上就是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;

};

Here we are. As you can see, the Test method generated by C# in its binary form is of the [out, retval] form as expected. The rest is all sugar and wrappers. Most COM interfaces methods are, at binary level, designed using [out, retval] because compilers don't support a common compatible binary format for function return.

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

What VBE7 defines is a dispinterface, again some form of syntactic sugar for defining interfaces on top of COM raw/binary IUnknown interface. The only mystery left is why CDec is defined differently than other methods in VBE7. I don't have an answer for that.

VBE7所定义的是一个非接口,这也是在COM raw/二进制IUnknown接口上定义接口的一种语法。唯一的谜团是为什么CDec的定义与VBE7中的其他方法不同。对此我没有答案。

Now, specifically about the module keyword in IDL, IDL is just an abstract definitions (functions, constants, classes, etc.) tool that optionally outputs artefacts (.H, .C, .TLB, etc.) targeted for a specific language (C/C++, etc.) or for specific clients.

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

It happens that VB/VBA supports TLB's constants and methods. It interprets constants as what they are, and functions in modules as DLL exports from the module's dll name.

碰巧VB/VBA支持TLB的常量和方法。它将常量解释为它们是什么,并将模块中的函数作为DLL导出,从模块的DLL名称中导出。

So if you create this my.idl file somewhere on your disk:

如果你创建这个。磁盘上某处的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();
    }
}

You can compile a TLB from it like this:

你可以这样编译一个TLB:

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

It will create a my.tlb file that you can reference in VB/VBA. Now from VB/VBA, you have a new function available (intellisense will work on it) called GetCurrentThreadId. It works because Windows' kernel32.dll does export a GetCurrentThreadId function.

它会产生一个my。可以在VB/VBA中引用的tlb文件。现在,从VB/VBA中,有一个新的函数可用(intellisense可以处理它),名为GetCurrentThreadId。这是因为Windows的内核32。dll输出一个GetCurrentThreadId函数。

You can only create DLL Exports from C/C++ projects (and from other languages/tools like Delphi), but not from VB/VBA, not from .NET.

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

In fact there are some tricks to create exports in .NET, but it's not really standard: Is is possible to export functions from a C# DLL like in VS C++?

实际上,有一些技巧可以在。net中创建导出,但是它并不是真正的标准:是否可以从c# DLL中导出函数,比如在VS c++中?

#1


5  

What's important in CDec declaration is the [out] and [retval] attributes combined. Tools that understand it (like VB/VBA) will be capable of compiling calls to this method in a simplified way, masking error handling, so

CDec声明中重要的是[out]和[retval]属性的组合。理解它的工具(如VB/VBA)能够以一种简化的方式编译对该方法的调用,从而屏蔽错误处理

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

is equivalent to

相当于

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

equivalent here does not mean it's equivalent in binary form, it just means the tools that understand that syntax will be ok to use (and compile in the final binary target) the first expression when they see the second. It also implies that if there is an error (HRESULT failure) then the tool should raise an error by any way it sees fit (VB/VBA will do this).

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

That's simply "syntactic sugar".

这是简单的“语法”。

You can write that using MIDL, but also .NET: just create a standard Class Library using Visual Studio and add this sample c# class:

您可以使用MIDL,但也可以使用。net:使用Visual Studio创建一个标准类库,并添加这个c#类示例:

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

Compile that and run the regasm tool to register it, with a command like this:

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

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

This will register the class as a COM object, and create a C:\mypath\ClassLibrary1\bin\Debug\classlibrary1.tlb type library file.

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

Now, start Excel (you could use any COM automation compatible client), and add a reference to the ClassLibrary1 (developer mode, VBA editor, Tools / Reference). If you don't see it you may be running with a different bitness. It's possible to use COM for 32-64 communication, but for now, just make sure your client runs at the same bitness as how your ClassLibrary1.dll was compiled.

现在,启动Excel(您可以使用任何与COM自动化兼容的客户端),并向ClassLibrary1添加一个引用(开发人员模式、VBA编辑器、工具/引用)。如果你没有看到它,你可能会以不同的方式运行。使用COM进行32-64通信是可能的,但就目前而言,只需确保您的客户端与ClassLibrary1的运行方式相同。dll被编译。

Once you have the reference, add some VB code, like this.

一旦有了引用,就添加一些VB代码,像这样。

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

As you will experience, VB intellisense will show the method like we expect, like in C#, and it works fine.

正如您将看到的,VB intellisense将会像我们所期望的那样显示这个方法,就像c#一样,并且它工作得很好。

Now, let's try to use it from C++: create a console project (again, make sure the bitness is compatible), and add this code to it:

现在,让我们尝试从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;
}

This will also work fine and the code looks close to VB's one. Notice I used Visual Studio magic #import directive which is super cool because it masks many details of COM Automation plumbing (just like VB/VBA does), including bstr and variant smart classes.

这也会很好地工作,并且代码看起来很接近VB的代码。注意,我使用了Visual Studio magic #import指令,它非常酷,因为它掩盖了COM自动化管道的许多细节(就像VB/VBA那样),包括bstr和变体智能类。

Let's click on the Test call and do a Goto Definition (F12), this is what we see:

让我们点击测试调用并做一个Goto定义(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);
}

haha! This is basically what VB/VBA does also undercovers. We can see how exception handling is done. Again, if you do an F12 on _Class1Ptr, this is what you'll see (simplified):

哈哈!这基本上就是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;

};

Here we are. As you can see, the Test method generated by C# in its binary form is of the [out, retval] form as expected. The rest is all sugar and wrappers. Most COM interfaces methods are, at binary level, designed using [out, retval] because compilers don't support a common compatible binary format for function return.

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

What VBE7 defines is a dispinterface, again some form of syntactic sugar for defining interfaces on top of COM raw/binary IUnknown interface. The only mystery left is why CDec is defined differently than other methods in VBE7. I don't have an answer for that.

VBE7所定义的是一个非接口,这也是在COM raw/二进制IUnknown接口上定义接口的一种语法。唯一的谜团是为什么CDec的定义与VBE7中的其他方法不同。对此我没有答案。

Now, specifically about the module keyword in IDL, IDL is just an abstract definitions (functions, constants, classes, etc.) tool that optionally outputs artefacts (.H, .C, .TLB, etc.) targeted for a specific language (C/C++, etc.) or for specific clients.

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

It happens that VB/VBA supports TLB's constants and methods. It interprets constants as what they are, and functions in modules as DLL exports from the module's dll name.

碰巧VB/VBA支持TLB的常量和方法。它将常量解释为它们是什么,并将模块中的函数作为DLL导出,从模块的DLL名称中导出。

So if you create this my.idl file somewhere on your disk:

如果你创建这个。磁盘上某处的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();
    }
}

You can compile a TLB from it like this:

你可以这样编译一个TLB:

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

It will create a my.tlb file that you can reference in VB/VBA. Now from VB/VBA, you have a new function available (intellisense will work on it) called GetCurrentThreadId. It works because Windows' kernel32.dll does export a GetCurrentThreadId function.

它会产生一个my。可以在VB/VBA中引用的tlb文件。现在,从VB/VBA中,有一个新的函数可用(intellisense可以处理它),名为GetCurrentThreadId。这是因为Windows的内核32。dll输出一个GetCurrentThreadId函数。

You can only create DLL Exports from C/C++ projects (and from other languages/tools like Delphi), but not from VB/VBA, not from .NET.

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

In fact there are some tricks to create exports in .NET, but it's not really standard: Is is possible to export functions from a C# DLL like in VS C++?

实际上,有一些技巧可以在。net中创建导出,但是它并不是真正的标准:是否可以从c# DLL中导出函数,比如在VS c++中?