如何在 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++?
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++?