COM基础知识
Microsoft 组件对象模型 (COM) 是一个独立于平台的分布式面向对象的系统,用于创建可以交互的二进制软件组件。它定义了一个二进制互操作性标准,用于创建在运行时交互的可重用软件库。
COM 定义 COM 对象的基本性质。 通常,软件对象由一组数据和操作数据的函数组成。 COM 对象是一个对象,在该对象中,只能通过一组或多组相关函数访问对象的数据。 这些函数集称为 接口,接口的函数称为 方法。 此外,COM 要求访问接口方法的唯一方法是通过指向 接口的指针。
COM 独立于实现语言,这意味着可以通过使用不同的编程语言(如 C++ 和 .NET Framework中的编程语言)创建 COM 库。
对象和接口
COM 对象通过 接口公开其功能,接口是成员函数的集合。 COM 接口定义组件的预期行为和职责,并指定提供一小部分相关操作的强类型协定。 COM 组件之间的所有通信都通过接口进行,组件提供的所有服务都通过其接口公开。 调用方只能访问接口成员函数。 内部状态对调用方不可用,除非它在接口中公开。
接口是强类型。 每个接口都有其自己唯一的接口标识符(名为 IID).IID 是 GUID (全局唯一标识符)。 创建新接口时,必须为该接口创建新的标识符。 当调用方使用接口时,它必须使用唯一标识符。
定义新接口时,可以使用接口定义语言 (IDL) 来创建接口定义。Microsoft 提供的 IDL 基于 DCE IDL 的简单扩展,DCE IDL 是远程过程调用 (RPC) 分布式计算的行业标准。
IUnknown 接口
所有 COM 接口都继承自 IUnknown 接口。 IUnknown 接口有三个成员函数,名为 QueryInterface、AddRef 和 Release。
QueryInterface 成员函数为 COM 提供多态性。 调用 QueryInterface 以在运行时确定 COM 对象是否支持特定接口。 如果 COM 对象实现请求的接口, ppvObject 则它将返回 参数中的接口指针,否则返回 NULL
COM
C# 注册COM对象需要声明类接口、“事件接口”(如有必要)和类本身。 类成员必须遵循以下规则才能在 COM 中显示:
1
2
3
4
类必须是公开的。
属性、方法和事件必须是公开的。
必须在类接口上声明属性和方法。
必须在事件接口中声明事件。
如果该类中声明了其他的公共成员,但没有在接口中声明, 则对 COM 不可见,但它们对其他 .NET 对象可见。
如果想对 COM 公开属性和方法,则必须在类接口上声明这些属性和方法,将它们标记为 DispId 属性,并在类中实现它们。你在接口中声明的成员的顺序是用于 COM vtable 的顺序。
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using System.Runtime.InteropServices;
namespace project_name
{
[Guid("EAA4976A-45C3-4BC5-BC0B-E474F4C3C83F")]
public interface ComClass1Interface
{
}
[Guid("0D53A3E8-E51A-49C7-944E-E72A2064F938"),
ClassInterface(ClassInterfaceType.None)]
public class ComClass1 : ComClass1Interface
{
}
}
Register
Register for COM Interop
The Register for COM interop 项目属性指定是否将你的应用程序公开为COM对象 (a COM-callable wrapper 一个COM可调用包装器) 从而允许其他COM对象与你的应用程序进行交互。
在C#工程里,该属性是在Build选项页里。

Make assembly COM visible
在C#里还有另外一个属性,在项目属性 > Application tab > Assembly Information button > check “Make assembly COM-Visible”.

Make assembly COM visible 使得程序集里所有public方法都COM可见,但我们在实际中很少这样操作,通常只在需要COM可见的对象上设置ComVisible属性。
1
2
3
4
[ComVisible(true)]
public interface IMyInterface
{
}
Register for COM interop 相当于执行 regasm,将程序集注册为注册表中的 COM 组件。
当编译生成dll后,需要对其进行注册,这样COM客户端才能找到它。每个COM Object都有一个唯一标识GUID,需要找出是哪个 DLL 实现了它。这些信息都记录在注册表 HKLM\Software\Classes\CLSID\{guid} 。

可以通过运行 Regasm.exe /codebase /tlb xxx.dll 来完成此操作,也可以直接勾选此选项,让VS自动完成。VS会在编译前先反注册掉旧的接口,编译成功后重新注册新的dll,这样可以防止注册表污染。
属性介绍
- ComVisible(true) 设置接口是否COM可见
- ComInterfaceType
确定如何向 COM 公开接口。- InterfaceIsDual 指示该接口作为双接口,能够早期绑定和后期绑定。 InterfaceIsDual 是默认值。
- InterfaceIsIDispatch 仅启用后期绑定。
- InterfaceIsIInspectable 作为 Windows 运行时接口向 COM 公开
- InterfaceIsIUnknown 作为从 IUnknown 派生的接口向 COM 公开,这仅支持早期绑定
默认情况下, Tlbexp.exe (类型库导出程序) 将托管接口作为双重接口公开给 COM,使你能够灵活地后期绑定或提前绑定。
- ClassInterfaceType
标识为类生成的接口的类型。- AutoDispatch
表示该类仅支持 COM 客户端的后期绑定。 该类的调度接口会根据请求自动向 COM 客户端公开。 Tlbexp.exe(类型库导出器)生成的类型库不包含调度接口的类型信息,以防止客户端缓存接口的 DISPID。 调度接口不会出现ClassInterfaceAttribute 中描述的版本控制问题,因为客户端只能后期绑定到该接口。
这是 ClassInterfaceAttribute 的默认设置。 - AutoDual
表示为该类自动生成双类接口并暴露给COM。 为类接口生成类型信息并在类型库中发布。 由于 ClassInterfaceAttribute 中描述的版本控制限制,强烈建议不要使用 AutoDual。 - None
表示没有为该类生成类接口。 如果没有显式实现任何接口,则该类只能通过 IDispatch 接口提供后期绑定访问。 这是 ClassInterfaceAttribute 的推荐设置。 使用 ClassInterfaceType.None 是通过类显式实现的接口公开功能的唯一方法。
- AutoDispatch
实例
- 创建一个.net Framework的class library工程,命名为DemoCom
-
删掉默认创建的Class1, 新加一个接口interface,命名为ICalculate,将接口设为public,添加COM属性,并添加一个新的方法
1 2 3 4 5 6 7 8 9 10 11
using System.Runtime.InteropServices; namespace DemoCom { [ComVisible(true)] [InterfaceType(ComInterfaceType.InterfaceIsDual)] public interface ICalculate { int Sum(int a, int b); } } -
增加一个新的类Calculate,添加COM属性,继承实现接口ICalculate。注意这里我们多加了一个public方法Substraction,这个方法是COM不可见的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
using System.Runtime.InteropServices; namespace DemoCom { [ComVisible(true)] [ClassInterface(ClassInterfaceType.None)] public class Calculate:ICalculate { public int Sum(int a, int b) { return a + b; } public int Substract(int a, int b) { return a - b; } } } - 修改工程属性,将
Register for COM interop勾选上, 到Sign选项卡,勾选Sign the assembly, 创建一个新的key

- 编译工程,在bin\Release下生成DemoCom.dll和DemoCom.tlb文件
-
新建一个.net framework的console工程,命名DemoComTest
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
static void Main(string[] args) { try { string calculateProgID = "DemoCom.Calculate"; Type calculateManagerType = Type.GetTypeFromProgID(calculateProgID); ICalculate calculate = (ICalculate)Activator.CreateInstance(calculateManagerType); Console.WriteLine("Call com interface, get result: " + calculate.Sum(1, 2)); } catch(Exception ex) { Console.WriteLine("Failed to call COM interface, error message: " + ex.Message); } Console.ReadKey(); } - 将Console工程设为启动项,运行,返回正确结果3
-
我们的C# COM接口可以被C++调用,创建一个c++ Console project,命名DemoCoreTest2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
#include <iostream> #include <string> #import "C:\Users\KZhang4\source\repos\KerwenComDemo\DemoCom\bin\Release\DemoCom.tlb" inline void TESTHR(HRESULT x) { if FAILED(x) _com_issue_error(x); }; int main() { try { TESTHR(CoInitialize(0)); DemoCom::ICalculatePtr CalculatePtr = nullptr; TESTHR(CalculatePtr.CreateInstance("DemoCom.Calculate")); long result = CalculatePtr->Sum(1, 2); std::string test1 = std::to_string(result); std::cout << "Call C# COM interface, get result: " + test1 + "\n"; } catch (const _com_error& e) { std::cout << "Failed to call C# COM interface, Exception occurred.\n"; } CoUninitialize();// Uninitialize COM return 0; }
COM+
COM+不是一项新技术,它是对当前com技术的一个扩充。
COM+中增加的主要东西包括两种已有的技术,微软事务服务器(MTS)和微软消息对列(MSMQ)。MTS通过事务增加了COM的可靠性。COM+的底层结构仍然以COM为基础,它几乎包容了COM的所有内容,而且不再局限于COM的组件技术,它更加注重于分布式网络应用的设计和实现。COM+综合了COM、DCOM和MTS这些技术要素,它把COM组件软件提升到应用层而不再是底层的软件结构,它通过操作系统的各种支持,使组件对象模型建立在应用层上,把所有组件的底层细节留给操作系统,因此,COM+与操作系统的结合更加紧密。
COM+ 管理器
“控制面板”-“管理工具”-“组件服务”。这是 COM+ 管理器。展开本地计算机并浏览到包含 COM 对象的本地 COM+ 应用程序。在这些组件中,您可以查看它们实现的 COM 接口以及这些组件上的方法。
Enterprise Services (COM+) in .NET
.Net Enterprise Services提供了可以在.Net 组件中使用的COM+服务。因为它也是基于以前的COM+技术,在.NET平台上开发的.NET组件。使用Enterprise Services可以将.NET组件并进行封装为COM对象,这样.NET组件就可以使用COM+服务了。
Enterprise Services里最常用的特性就是自动事务处理。为编写使用事务服务的托管应用程序,必须从 ServicedComponent 中派生需要服务的类。ServicedComponent是所有使用COM+服务类的基类。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using System.EnterpriseServices;
[assembly: ApplicationName("BankComponent")]
[assembly: AssemblyKeyFileAttribute("Demos.snk")]
namespace BankComponentServer
{
[Transaction(TransactionOption.Required)]
public class Account : ServicedComponent
{
[AutoComplete]
public bool Post(int accountNum, double amount)
{
// Updates the database, no need to call SetComplete.
// Calls SetComplete automatically if no exception is thrown.
}
}
}
以上代码显示了在 .NET 中使用事务的 Account 类的实现。
- ApplicationName 将此程序与COM+ 应用程序关联起来。
- Account 类是从 System.EnterpriseServices.ServicedComponent 类中派生的。
- Transaction 将该类标记为需要一个事务。由于使用了 Transaction 属性,所以将自动配置同步和 JIT 服务。
- AutoComplete 属性用于指定:如果在方法执行过程中出现未处理的异常,运行时必须为该事务自动调用 SetAbort 函数,否则,将调用 SetComplete 函数。
关键属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
[assembly: ApplicationName("ObjectInspector")]
[assembly: ApplicationActivation(ActivationOption.Server)]
[assembly: System.Reflection.AssemblyKeyFile("Inspector.snk")]
[Transaction(TransactionOption.RequiresNew)]
[ObjectPooling(true, 5, 10)]
public class EmployeeMaintenance : ServicedComponent
{
[AutoComplete(true)]
public void AddEmployee(...)
{
}
}
-
TransactionOption 指定组件请求的自动事务类型。
- Disabled 忽略当前上下文中的任何事务
- NotSupported 在没有管理事务的上下文中创建组件。
- Required 如果存在事务, 则共享,必要时创建新事务。
- RequiresNew 直接使用新事务创建组件,而不考虑当前上下文的状态。
- Supported 如果存在事务, 则共享
- ObjectPooling 为组件启用和配置对象池。 可配置参数包括 Enabled、MaxPoolSize、MinPoolSize 和 CreationTimeout。。如果未指定任何值,则将使用 COM+ 默认值 (启用为 true,最小池大小为 0,最大池大小为 1,048,576,创建超时为 60 毫秒)
-
AutoComplete是否自动完成事务,如果方法调用正常返回,则事务会自动调用 SetComplete。如果方法调用引发异常,则事务将中止。如果AutoComplete设置为 false,或者将其全部省略,那么我们将需要手动管理事务。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
public void SampleFunction() { try { // Do something to a database // ... // Everything okay so far Commit the transaction ContextUtil.SetComplete(); } catch(Exception) { // Something went wrong Abort and Rollback the Transaction. ContextUtil.SetAbort(); } } - ApplicationName 指定COM+应用程序的名称。
- ApplicationActivation 指定COM+组件是在创建者的进程中运行,还是在系统进程中运行。
- Library 指定在创建者的进程中激活COM+服务组件。(默认为Library)
- Server 指定在系统提供的进程中激活COM+服务组件。
- EventTrackingEnabled 为组件启用事件跟踪
- [Synchronication(SynchronizationOption.Required)] 多线程托管组件可以使用.NET 提供的同步锁,例如经典事件和互斥锁。 但是,service组件应该通过将 Synchronization 属性添加到类定义来实现基于 COM+ 活动的同步。
注册
组件如果想为 COM+ 应用程序提供服务,必须注册。注册过程可以是以下三种方式:
- 使用 RegSvcs.exe 命令行实用工具手动注册。
- 通过.net 客户端应用程序动态注册。
- 编程代码注册。
手动注册
手动注册服务组件是使用 RegSvcs.exe 命令行。
1
regsvcs.exe /fc MyApp SomeAssembly.dll
如果程序没有在代码中提供应用程序名称,则必须用 /appname显式告知 RegSvcs.exe COM+应用程序的名称
1
regsvcs.exe /appname:MyApp SomeAssembly.dll
如果 既没有在代码里指定,也没有用RegSvcs.exe 命令行参数,.NET 将使用工程的namespace作为 COM+ 应用程序名称。
默认情况下,使用 RegSvcs.exe 注册时 如果原先已经注册过 COM+ 应用程序,RegSvcs.exe 不会更改其应用设置。如果想重新配置现有版本,则添加 /reconfig:
1
regsvcs.exe /reconfig /fc MyApp MyAssembly.dll
动态注册
当C# 客户端程序尝试创建COM+服务组件时,.NET 将尝试解析要用于该程序集的版本。如果该COM+组件未注册,运行时将自动尝试将其注册到 COM+ 目录。这个过程称为动态注册。 与 RegSvcs.exe 类似,如果程序集在属性中包含了 COM+ 应用程序名称,则使用该名称。否则,COM+ 应用程序将使用程序集的名称。
注意
- 只有 .NET 客户端才能使用服务组件的动态注册。 其他非托管客户端必须使用 RegSvcs.exe。
- 动态注册要求管理员权限。
实例
- 创建一个C# Library 工程 DemoComPlus
- 添加System.EnterpriseServices引用
-
删除默认添加的Class1,添加一个新的接口文件ICalculate,添加相关COM+属性
1 2 3 4 5 6 7 8 9 10
using System.Runtime.InteropServices; namespace DemoComPlus { [ComVisible(true)] public interface ICalculate { int sum(int a, int b); } } -
添加接口实现文件Calculate,继承ServicedComponent,添加COM+属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
using System.EnterpriseServices; using System.Runtime.InteropServices; [assembly: ApplicationName("DemoComPlus.Calculate")] [assembly: ApplicationActivation(ActivationOption.Library)] namespace DemoComPlus { [Transaction(TransactionOption.RequiresNew)] [ObjectPooling(true, 5, 10)] [ComVisible(true)] public class Calculate : ServicedComponent, ICalculate { public int sum(int a, int b) { return a + b; } } } - 添加强签名

- 添加一个C# Console工程DemoComPlusTest
-
调用COM+ 接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
class Program { static void Main(string[] args) { try { ICalculate calculate = new Calculate(); Console.WriteLine("Call COM+ interface, get result: " + calculate.sum(1, 2)); } catch (Exception ex) { Console.WriteLine("Failed to call COM+ interface, error message: " + ex.Message); } Console.ReadKey(); } } - 编译运行Console工程
- 打开控制面板-Admin Tool - Component Services - COM+ applications,我们编写的COM+ dll已经自动注册为组件

Reference
COM:
COM指南 official
Example COM Class
How to: Register a Component for COM Interop
“Register for COM Interop” vs “Make assembly COM visible”
InterfaceTypeAttribute Class
COM编程攻略
Turn a simple C# DLL into a COM interop component
C# Com and COM+
COM+
COM+ official
.NET Serviced Components
Understanding Enterprise Services (COM+) in .NET
了解 .NET 中的企业服务 (COM+)
Creating COM+ Objects using EnterpriseServices in .NET
Creating a Simple COM+ Application
Building a complete COM+ Server component using C# and .NET
C#中写COM+组件
Accessing COM+ component using C#
C# Com and COM+
Creating a Sample COM+ Service
通过有意的造成主键重复,导致事务自动回滚的效果