C#

使用C#编写COM和COM+ Application

Posted by Kerwen Blog on November 14, 2023

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选项页里。
image

Make assembly COM visible

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

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}
image

可以通过运行 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 是通过类显式实现的接口公开功能的唯一方法。

实例

  1. 创建一个.net Framework的class library工程,命名为DemoCom
  2. 删掉默认创建的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);
         }
     }
    
  3. 增加一个新的类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;
             }
         }
     }
    
  4. 修改工程属性,将Register for COM interop勾选上, 到Sign选项卡,勾选Sign the assembly, 创建一个新的key
    image
  5. 编译工程,在bin\Release下生成DemoCom.dll和DemoCom.tlb文件
  6. 新建一个.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();
     }
    
  7. 将Console工程设为启动项,运行,返回正确结果3
  8. 我们的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(...)
    {
    }
}
  1. TransactionOption 指定组件请求的自动事务类型。

    • Disabled 忽略当前上下文中的任何事务
    • NotSupported 在没有管理事务的上下文中创建组件。
    • Required 如果存在事务, 则共享,必要时创建新事务。
    • RequiresNew 直接使用新事务创建组件,而不考虑当前上下文的状态。
    • Supported 如果存在事务, 则共享
  2. ObjectPooling 为组件启用和配置对象池。 可配置参数包括 Enabled、MaxPoolSize、MinPoolSize 和 CreationTimeout。。如果未指定任何值,则将使用 COM+ 默认值 (启用为 true,最小池大小为 0,最大池大小为 1,048,576,创建超时为 60 毫秒)
  3. 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();
         }
     }
    
  4. ApplicationName 指定COM+应用程序的名称。
  5. ApplicationActivation 指定COM+组件是在创建者的进程中运行,还是在系统进程中运行。
    • Library 指定在创建者的进程中激活COM+服务组件。(默认为Library)
    • Server 指定在系统提供的进程中激活COM+服务组件。
  6. EventTrackingEnabled 为组件启用事件跟踪
  7. [Synchronication(SynchronizationOption.Required)] 多线程托管组件可以使用.NET 提供的同步锁,例如经典事件和互斥锁。 但是,service组件应该通过将 Synchronization 属性添加到类定义来实现基于 COM+ 活动的同步。

注册

组件如果想为 COM+ 应用程序提供服务,必须注册。注册过程可以是以下三种方式:

  1. 使用 RegSvcs.exe 命令行实用工具手动注册。
  2. 通过.net 客户端应用程序动态注册。
  3. 编程代码注册。

手动注册

手动注册服务组件是使用 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。
  • 动态注册要求管理员权限。

实例

  1. 创建一个C# Library 工程 DemoComPlus
  2. 添加System.EnterpriseServices引用
  3. 删除默认添加的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);
         }
     }
    
  4. 添加接口实现文件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;
             }
         }
     }
    
  5. 添加强签名
    image
  6. 添加一个C# Console工程DemoComPlusTest
  7. 调用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();
         }
     }
    
  8. 编译运行Console工程
  9. 打开控制面板-Admin Tool - Component Services - COM+ applications,我们编写的COM+ dll已经自动注册为组件
    image

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
通过有意的造成主键重复,导致事务自动回滚的效果