前言
.Net 类库功能非常全面分装了大量应用级别的API,所以有时会有在VC 中调用托管程序集(.Net Managed Assemblies) 的需求。本文通过示例对该步骤进行说明,并提供一些参考。
前提
本文假设已有一个现成可用的托管程序集,代码如下:
// 媒体类:用来载入一个媒体文件,并解析出信息
public class Media
{
// 内部处理
private void DoSomething()
{ ... }
// 载入文件
public void LoadMedia(string filePath)
{ ... }
// 获取媒体信息
public MediaInfo GetInfo()
{ ... }
}
// 媒体信息类
public class MediaInfo
{
// 内部标示
private Guid id;
// 文件名
public property string Name { get; set; }
}
托管程序集(.Net Managed Assemblies)
1 追加 ComVisible Attribute
要使一个托管程序集能作为 COM 被 VC 调用首先需要添加 ComVisible Attribute。如果你比较懒可以在项目的 AssemblyInfo.cs 文件中进行全局声明:
[assembly: ComVisible(true)]
[assembly: Guid("2aa76d89-5faf-473e-8285-b9199fedd365")]
当程序集的公有类相当多的时候这是个好办法,但是并不建议这样做,因为调用方(VC)通常并不需要访问程序集中的所有公有类,并且过多的暴露程序集也不太好。这里我们就勤劳点,在每个类上进行声明。
[ComVisible(true)]
public class Media : IDisposable
{
// 略
}
[ComVisible(true)]
public class MediaInfo
{
// 略
}
2 追加 ClassInterface Attribute
参照一下 MSDN 该 Attribute 需要传入一个 ClassInterfaceType 枚举来决定生成接口的方式:
- AutoDual 指示自动为类生成双重类接口并向 COM 公开。为该类接口生成类型信息并在类型库中发布。由于 ClassInterfaceAttribute 中描述的版本控制方面的限制,极力建议不要使用 AutoDual。
-
AutoDispatch 指示该类只支持 COM 客户端的后期绑定。在请求时,该类的调度接口将自动向 COM 客户端公开。类型 类型库导出程序 (Tlbexp.exe) 生成的类型库不包含调度接口的类型信息,以防止客户端缓存接口的 DISPID。由于客户端只能后期绑定到调度接口,因此该接口不会出现 ClassInterfaceAttribute 中所述的版本控制问题。
这是 ClassInterfaceAttribute 的默认设置。 - None 指示不为类生成类接口。如果未显式实现任何接口,则该类将只通过 IDispatch 接口提供后期绑定访问。这是 ClassInterfaceAttribute 的推荐设置。
既然官方推荐,那么我们就采用ClassInterfaceType.None,但是因为类接口不再自动生成,所以我们需要自己为 2 个类分别定义 2 个接口:
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
[ComDefaultInterface(typeof(IMedia))]
public class Media : IDisposable, IMedia
{
// 略
}
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
public class MediaInfo : IMediaInfo
{
// 略
}
[ComVisible(true)]
public interface IMedia
{
void LoadMedia(string filePath);
MediaInfo GetInfo();
}
[ComVisible(true)]
public interface IMediaInfo
{
property string Name { get; set; }
}
提示:
- 接口只需要定义公开给 Com 的接口,即如果VC 不需要调用 LoadMedia 方法,那么IMedia 接口中可以不包含该方法
- 接口本身也需要 ComVisible 声明
- 当某个类实现了多个接口的情况下,需要使用 ComDefaultInterface 明确指定 COM 接口是哪一个
3 生成支持 COM 调用托管程序集(.Net Managed Assemblies)
使用 VS 的话相当简单,在工程的属性页面里“Register for COM interop”打个钩就一切搞定,编译后去 bin 下面就能找到 .NET 程序集,以及一个同名的 .tlb 文件。
当然你也需要了解下怎样使用命令行工具来实现,毕竟配布到用户机上时会用到。这里介绍下程序集注册工具 (Regasm.exe),VS 实际上帮我们运行了以下命令:
regasm myTest.dll /tlb:myTest.tlb
MSDN 官方描述为:注册 myTest.dll 中包含的所有公共类,并生成和注册类型库 myTest.tlb,该类型库包含 myTest.dll 中定义的所有公共类型的定义。
COM 类注册自然是更新注册表了,那配布的时候用户机上同样需要更新注册表,那么怎样知道需要更新哪些信息呢?Regasm.exe 会告诉你:
regasm myTest.dll /regfile:myTest.reg
这个命令会生成一个注册表文件,需要更新的内容都在里面了。好了该说 VC 怎样调用了。
VC 调用托管程序集
方便起见我们用空的一个命令行 VC 工程来说明步骤。
1 添加 import 声明,并生成 com 接口
在 .cpp 文件中添加以下内容并编译(生成物中会包含一个 .tlh 的头文件):
#import "myTest.tlb" named_guids raw_interfaces_only
PS:这里的文件路径可以是绝对路径,也可以是相对路径。
如果你还引用到一些 .NET 类库中的类型那么也需要追加 import 声明,譬如使用了 System.Drawing 中的类型,那么要追加:
#import "C:\Windows\Microsoft.NET\Framework\v4.0.30319\System.Drawing.tlb" named_guids raw_interfaces_only
2 代码调用
HRESULT hRes;
hRes = CoInitialize(NULL);// 初始化 Com
MyTest::IMediaPtr pIMedia(__uuidof(MyTest::Media));// 使用智能指针实例化对象
pIMedia->LoadMedia(::SysAllocString(L"d:\\aaa.png"));// 调用方法,OK
MyTest::IMediaInfoPtr pIMediaInfo = pIMedia->GetInfo();// 调用方法并返回值,NG
CoUninitialize();// 终止初始化 Com
调用没有返回值的方法时一切正常,调用有返回值的方法时你会发现函数定义被改写了,这是什么情况?原来从 .NET 程序集转换为 com 时,如果有返回值,这个返回值将作为ref参数追加在该函数之后,而返回值会用来返回 HRESULT。这虽然没啥大问题,但用起来却大大的不爽,想用回原来调用方式的情看下文。
托管程序集(.Net Managed Assemblies)再修改
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
[ComDefaultInterface(typeof(IMedia))]
public class Media : IDisposable, IMedia
{
// 略
[PreserveSig]
public MediaInfo GetInfo()
{ ... }
}
PreserveSig 可以保证转换的 com 接口不改写原来的方法定义,详细请看http://msdn.microsoft.com/zh-cn/library/system.runtime.interopservices.dllimportattribute.preservesig(v=vs.80).aspx
现在我们再次编译 .NET ,打开 VC 工程,并确认两个方法都能正确调用了。