C# COM 实现可枚举而不引用 MSCORLIB
C# COM implement enumerable without referencing MSCORLIB
我正在创建一个 COM 接口,它应该允许在 Visual Basic 脚本中使用 For Each
并在 C++ 中使用 IEnumVariant
。问题是我不希望 C++ 客户端应用程序需要导入 mscorlib.tlb.
到目前为止我的界面是:
[ComVisible(true)]
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface ICars : System.Runtime.InteropServices.ComTypes.IEnumVARIANT
{
int Count { get; }
}
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
public class Cars : ICars
{
int ICars.Count => throw new NotImplementedException();
int IEnumVARIANT.Next(int celt, object[] rgVar, IntPtr pceltFetched)
{
throw new NotImplementedException();
}
int IEnumVARIANT.Skip(int celt)
{
throw new NotImplementedException();
}
int IEnumVARIANT.Reset()
{
throw new NotImplementedException();
}
IEnumVARIANT IEnumVARIANT.Clone()
{
throw new NotImplementedException();
}
}
TlbExp 吐出这段代码:
// Generated .IDL file (by the OLE/COM Object Viewer)
//
// typelib filename: carsIEnumerator.tlb
[
uuid(3BBCEAA2-9498-48BF-8053-1CEFB3C1C86F),
version(1.0),
custom(90883F05-3D28-11D2-8F17-00A0C9A6186D, "ClassLibraryIEnumerator, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")
]
library ClassLibraryIEnumerator
{
// TLib : // TLib : mscorlib.dll : {BED7F4EA-1A96-11D2-8F08-00A0C9A6186D}
importlib("mscorlib.tlb");
// TLib : OLE Automation : {00020430-0000-0000-C000-000000000046}
importlib("stdole2.tlb");
// Forward declare all types defined in this typelib
interface ICars;
[
odl,
uuid(ABD2A9E4-D5C5-3ED9-88AF-4C310BD5792D),
version(1.0),
dual,
oleautomation,
custom(0F21F359-AB84-41E8-9A78-36D110E6D2F9, "ClassLibraryIEnumerator.ICars")
]
interface ICars : IDispatch {
[id(0x60020000), propget]
HRESULT Count([out, retval] long* pRetVal);
};
我怎样才能避免这种情况?
即使我只有自定义界面和一个 class(不使用任何 .NET 类型),引用仍然存在。
IEnumVARIANT 类型声明必须来自某处。它不是每个编译器都知道的标准类型,如 int
。如果您自己编写 IDL,那么您将使用 #import "oaidl.idl"
来包含定义。但这在 .NET 中行不通,因为类型库导出器不使用 IDL。所以它来自出口商确实知道的地方,mscorlib.tlb
解决方法是将接口声明放在您自己的代码中,而不是使用 mscorlib 中的接口声明。 Copy/paste 它来自 Reference Source 或这个:
[Guid("00020404-0000-0000-C000-000000000046")]
[InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
[ComImport]
public interface IEnumVARIANT
{
[PreserveSig]
int Next(int celt, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex=0), Out] object[] rgVar, IntPtr pceltFetched);
[PreserveSig]
int Skip(int celt);
[PreserveSig]
int Reset();
IEnumVARIANT Clone();
}
并在您的 ICars 声明中使用 YourNamespace.IEnumVARIANT。
声明自己的枚举器接口类型也是一种解决方案,IEnumVARIANT 不会赢得任何奖品。你可以放弃没有人使用的靠不住的方法,你可以让它成为类型安全的。如果您也控制客户端代码或者不必让脚本语言中的 foreach
满意,则这是一个可接受的替代方案。考虑:
[ComVisible(true)]
public interface ICarEnumerator {
ICar Next();
}
以及ICars界面中的ICarEnumerator GetCars()
最后但同样重要的是,考虑完全不实现迭代器。在客户端代码中让它看起来像一个数组:
[ComVisible(true)]
public interface ICars
{
int Count { get; }
ICar this[int index] { get; }
}
我也有同样的issue/need,我找到了这篇好文章。
https://limbioliong.wordpress.com/2011/10/28/exposing-an-enumerator-from-managed-code-to-com/
“我不希望 C++ 客户端应用程序需要导入 mscorlib.tlb。”
这是不可能的,因为您正在使用 .NET 创建 COM 组件类,它会自动将 mscorlib.tlb 和 mscoree.dll 引入游戏。用一个只能加两个整数的简单对象试试吧。
正如 Hans Passant 指出的那样,您根本不需要接口 IEnumVARIANT
。任何 COM 集合都必须基于 C# 集合,例如 List<T>
。这个 C# 集合有一个方法 GetEnumeration()
,它吐出一个 IEnumeration
对象作为 COM 中的 IEnumVARIANT
。您所要做的就是在接口中包含 IEnumerator GetEnumerator();
并将实现委托给 C# 集合的 GetEnumeration()
方法。
我在一个完整的例子中展示了这一点。考虑管理帐户集合的 Bank coclass。我需要 Bank、Account 和 AllAccounts 集合的 coclasses。
我从关键的 coclass AllAccounts 开始:
//AllAccounts.cs:
using System;
using System.Collections;
using System.Runtime.InteropServices;
namespace BankServerCSharp
{
[ComVisible(true)] // This is mandatory.
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface IAllAccounts
{
int Count{ get; }
[DispId(0)]
IAccount Item(int i);
[DispId(-4)]
IEnumerator GetEnumerator();
Account AddAccount();
void RemoveAccount(int i);
void ClearAllAccounts();
}
[ComVisible(true)] // This is mandatory.
[ClassInterface(ClassInterfaceType.None)]
public class AllAccounts:IAllAccounts
{
private AllAccounts(){ } // private constructor, coclass noncreatable
private List<IAccount> Al = new List<IAccount>();
public static AllAccounts MakeAllAccounts() { return new AllAccounts(); }
//public, but not exposed to COM
public IEnumerator GetEnumerator() { return Al.GetEnumerator(); }
public int Count { get { return Al.Count; } }
public IAccount Item(int i) { return (IAccount)Al[i - 1]; }
public Account AddAccount() { Account acc = Account.MakeAccount();
Al.Add(acc); return acc; }
public void RemoveAccount(int i) { Al.RemoveAt(i - 1); }
public void ClearAllAccounts() { Al.Clear(); }
}
}
默认 Item
方法和 GetEnumerator()
方法需要 DispId
值 0 和 -4。其他两个文件是:
Account.cs:
using System.Runtime.InteropServices;
namespace BankServerCSharp
{
[ComVisible(true)] // This is mandatory.
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface IAccount
{
double Balance { get; } // A property
void Deposit(double b); // A method
}
[ComVisible(true)] // This is mandatory.
[ClassInterface(ClassInterfaceType.None)]
public class Account:IAccount
{
private double mBalance = 0;
private Account() { } // private constructor, coclass noncreatable
public static Account MakeAccount() { return new Account(); }
//MakeAccount is not exposed to COM, but can be used by other classes
public double Balance { get { return mBalance; } }
public void Deposit(double b) { mBalance += b; }
}
}
Bank.cs:
using System.Runtime.InteropServices;
namespace BankServerCSharp
{
[ComVisible(true)] // This is mandatory.
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface IBank { IAllAccounts Accounts { get; } }
[ComVisible(true)] // This is mandatory.
[ClassInterface(ClassInterfaceType.None)]
public class Bank:IBank
{
private readonly AllAccounts All;
public Bank() { All = AllAccounts.MakeAllAccounts(); }
public IAllAccounts Accounts { get { return All; } }
}
}
您必须使用 x64 版本的 Regasm 注册服务器。
使用 C++ 测试服务器:
#include "stdafx.h"
#include <string>
#import "D:\Aktuell\CSharpProjects\BankServerCSharp\BankServerCSharp\bin\Release\BankServerCSharp.tlb"
//this is the path of my C# project's bin\Release folder
inline void TESTHR(HRESULT x) { if FAILED(x) _com_issue_errorex(x, nullptr, ID_NULL);}
int main()
{
try
{
TESTHR(CoInitialize(0));
BankServerCSharp::IBankPtr BankPtr = nullptr;
TESTHR(BankPtr.CreateInstance("BankServerCSharp.Bank"));
BankServerCSharp::IAllAccountsPtr AllPtr = BankPtr->Accounts;
BankServerCSharp::IAccountPtr FirstAccountPtr = AllPtr->AddAccount();
TESTHR(FirstAccountPtr->Deposit(47.11));
AllPtr->AddAccount();
TESTHR(AllPtr->Item[2]->Deposit(4711));
CStringW out, add;
for (int i = 1; i <= AllPtr->Count; i++)
{
add.Format(L"Balance of account %d: %.2f.\n", i, AllPtr->Item[i]->Balance);
out += add;
}
out += L"\n";
AllPtr->RemoveAccount(1);
for (int i = 1; i <= AllPtr->Count; i++)
{
add.Format(L"Balance of account %d: %.2f.\n", i, AllPtr->Item[i]->Balance);
out += add;
}
AllPtr->ClearAllAccounts();
add.Format(L"Number of accounts: %ld.\n", AllPtr->Count);
out += L"\n" + add;
MessageBoxW(NULL, out, L"Result", MB_OK);
//Raise an exception:
AllPtr->RemoveAccount(1);
}
catch (const _com_error& e)
{
MessageBoxW(NULL, L"Oops! Index out of range!", L"Error", MB_OK);
}
CoUninitialize();// Uninitialize COM
return 0;
}
备注:Item
是C++中的vector。我不知道如何将其更改为通常的函数形式,即 Item(i)
而不是 Item[i]
.
在VBA中,你可以使用心爱的For Each
循环:
Sub CSharpBankTest()
On Error GoTo Oops
Dim Out As String
Dim Bank As New BankServerCSharp.Bank 'New!
Dim AllAccounts As BankServerCSharp.AllAccounts 'No New!
Set AllAccounts = Bank.Accounts
Dim AccountOne As BankServerCSharp.Account 'No New
Set AccountOne = AllAccounts.AddAccount
AccountOne.Deposit 47.11
AllAccounts.AddAccount
AllAccounts(2).Deposit 4711
Dim i As Long
Dim ac As BankServerCSharp.Account
For Each ac In AllAccounts
i = i + 1
Out = Out & "Balance of account " & i & ": " & ac.Balance & vbNewLine
Next
Exit Sub
Oops:
MsgBox "Error Message : " & Err.Description, vbOKOnly, "Error"
End Sub
我正在创建一个 COM 接口,它应该允许在 Visual Basic 脚本中使用 For Each
并在 C++ 中使用 IEnumVariant
。问题是我不希望 C++ 客户端应用程序需要导入 mscorlib.tlb.
到目前为止我的界面是:
[ComVisible(true)]
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface ICars : System.Runtime.InteropServices.ComTypes.IEnumVARIANT
{
int Count { get; }
}
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
public class Cars : ICars
{
int ICars.Count => throw new NotImplementedException();
int IEnumVARIANT.Next(int celt, object[] rgVar, IntPtr pceltFetched)
{
throw new NotImplementedException();
}
int IEnumVARIANT.Skip(int celt)
{
throw new NotImplementedException();
}
int IEnumVARIANT.Reset()
{
throw new NotImplementedException();
}
IEnumVARIANT IEnumVARIANT.Clone()
{
throw new NotImplementedException();
}
}
TlbExp 吐出这段代码:
// Generated .IDL file (by the OLE/COM Object Viewer)
//
// typelib filename: carsIEnumerator.tlb
[
uuid(3BBCEAA2-9498-48BF-8053-1CEFB3C1C86F),
version(1.0),
custom(90883F05-3D28-11D2-8F17-00A0C9A6186D, "ClassLibraryIEnumerator, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")
]
library ClassLibraryIEnumerator
{
// TLib : // TLib : mscorlib.dll : {BED7F4EA-1A96-11D2-8F08-00A0C9A6186D}
importlib("mscorlib.tlb");
// TLib : OLE Automation : {00020430-0000-0000-C000-000000000046}
importlib("stdole2.tlb");
// Forward declare all types defined in this typelib
interface ICars;
[
odl,
uuid(ABD2A9E4-D5C5-3ED9-88AF-4C310BD5792D),
version(1.0),
dual,
oleautomation,
custom(0F21F359-AB84-41E8-9A78-36D110E6D2F9, "ClassLibraryIEnumerator.ICars")
]
interface ICars : IDispatch {
[id(0x60020000), propget]
HRESULT Count([out, retval] long* pRetVal);
};
我怎样才能避免这种情况?
即使我只有自定义界面和一个 class(不使用任何 .NET 类型),引用仍然存在。
IEnumVARIANT 类型声明必须来自某处。它不是每个编译器都知道的标准类型,如 int
。如果您自己编写 IDL,那么您将使用 #import "oaidl.idl"
来包含定义。但这在 .NET 中行不通,因为类型库导出器不使用 IDL。所以它来自出口商确实知道的地方,mscorlib.tlb
解决方法是将接口声明放在您自己的代码中,而不是使用 mscorlib 中的接口声明。 Copy/paste 它来自 Reference Source 或这个:
[Guid("00020404-0000-0000-C000-000000000046")]
[InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
[ComImport]
public interface IEnumVARIANT
{
[PreserveSig]
int Next(int celt, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex=0), Out] object[] rgVar, IntPtr pceltFetched);
[PreserveSig]
int Skip(int celt);
[PreserveSig]
int Reset();
IEnumVARIANT Clone();
}
并在您的 ICars 声明中使用 YourNamespace.IEnumVARIANT。
声明自己的枚举器接口类型也是一种解决方案,IEnumVARIANT 不会赢得任何奖品。你可以放弃没有人使用的靠不住的方法,你可以让它成为类型安全的。如果您也控制客户端代码或者不必让脚本语言中的 foreach
满意,则这是一个可接受的替代方案。考虑:
[ComVisible(true)]
public interface ICarEnumerator {
ICar Next();
}
以及ICars界面中的ICarEnumerator GetCars()
最后但同样重要的是,考虑完全不实现迭代器。在客户端代码中让它看起来像一个数组:
[ComVisible(true)]
public interface ICars
{
int Count { get; }
ICar this[int index] { get; }
}
我也有同样的issue/need,我找到了这篇好文章。
https://limbioliong.wordpress.com/2011/10/28/exposing-an-enumerator-from-managed-code-to-com/
“我不希望 C++ 客户端应用程序需要导入 mscorlib.tlb。”
这是不可能的,因为您正在使用 .NET 创建 COM 组件类,它会自动将 mscorlib.tlb 和 mscoree.dll 引入游戏。用一个只能加两个整数的简单对象试试吧。
正如 Hans Passant 指出的那样,您根本不需要接口 IEnumVARIANT
。任何 COM 集合都必须基于 C# 集合,例如 List<T>
。这个 C# 集合有一个方法 GetEnumeration()
,它吐出一个 IEnumeration
对象作为 COM 中的 IEnumVARIANT
。您所要做的就是在接口中包含 IEnumerator GetEnumerator();
并将实现委托给 C# 集合的 GetEnumeration()
方法。
我在一个完整的例子中展示了这一点。考虑管理帐户集合的 Bank coclass。我需要 Bank、Account 和 AllAccounts 集合的 coclasses。
我从关键的 coclass AllAccounts 开始:
//AllAccounts.cs:
using System;
using System.Collections;
using System.Runtime.InteropServices;
namespace BankServerCSharp
{
[ComVisible(true)] // This is mandatory.
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface IAllAccounts
{
int Count{ get; }
[DispId(0)]
IAccount Item(int i);
[DispId(-4)]
IEnumerator GetEnumerator();
Account AddAccount();
void RemoveAccount(int i);
void ClearAllAccounts();
}
[ComVisible(true)] // This is mandatory.
[ClassInterface(ClassInterfaceType.None)]
public class AllAccounts:IAllAccounts
{
private AllAccounts(){ } // private constructor, coclass noncreatable
private List<IAccount> Al = new List<IAccount>();
public static AllAccounts MakeAllAccounts() { return new AllAccounts(); }
//public, but not exposed to COM
public IEnumerator GetEnumerator() { return Al.GetEnumerator(); }
public int Count { get { return Al.Count; } }
public IAccount Item(int i) { return (IAccount)Al[i - 1]; }
public Account AddAccount() { Account acc = Account.MakeAccount();
Al.Add(acc); return acc; }
public void RemoveAccount(int i) { Al.RemoveAt(i - 1); }
public void ClearAllAccounts() { Al.Clear(); }
}
}
默认 Item
方法和 GetEnumerator()
方法需要 DispId
值 0 和 -4。其他两个文件是:
Account.cs:
using System.Runtime.InteropServices;
namespace BankServerCSharp
{
[ComVisible(true)] // This is mandatory.
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface IAccount
{
double Balance { get; } // A property
void Deposit(double b); // A method
}
[ComVisible(true)] // This is mandatory.
[ClassInterface(ClassInterfaceType.None)]
public class Account:IAccount
{
private double mBalance = 0;
private Account() { } // private constructor, coclass noncreatable
public static Account MakeAccount() { return new Account(); }
//MakeAccount is not exposed to COM, but can be used by other classes
public double Balance { get { return mBalance; } }
public void Deposit(double b) { mBalance += b; }
}
}
Bank.cs:
using System.Runtime.InteropServices;
namespace BankServerCSharp
{
[ComVisible(true)] // This is mandatory.
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface IBank { IAllAccounts Accounts { get; } }
[ComVisible(true)] // This is mandatory.
[ClassInterface(ClassInterfaceType.None)]
public class Bank:IBank
{
private readonly AllAccounts All;
public Bank() { All = AllAccounts.MakeAllAccounts(); }
public IAllAccounts Accounts { get { return All; } }
}
}
您必须使用 x64 版本的 Regasm 注册服务器。
使用 C++ 测试服务器:
#include "stdafx.h"
#include <string>
#import "D:\Aktuell\CSharpProjects\BankServerCSharp\BankServerCSharp\bin\Release\BankServerCSharp.tlb"
//this is the path of my C# project's bin\Release folder
inline void TESTHR(HRESULT x) { if FAILED(x) _com_issue_errorex(x, nullptr, ID_NULL);}
int main()
{
try
{
TESTHR(CoInitialize(0));
BankServerCSharp::IBankPtr BankPtr = nullptr;
TESTHR(BankPtr.CreateInstance("BankServerCSharp.Bank"));
BankServerCSharp::IAllAccountsPtr AllPtr = BankPtr->Accounts;
BankServerCSharp::IAccountPtr FirstAccountPtr = AllPtr->AddAccount();
TESTHR(FirstAccountPtr->Deposit(47.11));
AllPtr->AddAccount();
TESTHR(AllPtr->Item[2]->Deposit(4711));
CStringW out, add;
for (int i = 1; i <= AllPtr->Count; i++)
{
add.Format(L"Balance of account %d: %.2f.\n", i, AllPtr->Item[i]->Balance);
out += add;
}
out += L"\n";
AllPtr->RemoveAccount(1);
for (int i = 1; i <= AllPtr->Count; i++)
{
add.Format(L"Balance of account %d: %.2f.\n", i, AllPtr->Item[i]->Balance);
out += add;
}
AllPtr->ClearAllAccounts();
add.Format(L"Number of accounts: %ld.\n", AllPtr->Count);
out += L"\n" + add;
MessageBoxW(NULL, out, L"Result", MB_OK);
//Raise an exception:
AllPtr->RemoveAccount(1);
}
catch (const _com_error& e)
{
MessageBoxW(NULL, L"Oops! Index out of range!", L"Error", MB_OK);
}
CoUninitialize();// Uninitialize COM
return 0;
}
备注:Item
是C++中的vector。我不知道如何将其更改为通常的函数形式,即 Item(i)
而不是 Item[i]
.
在VBA中,你可以使用心爱的For Each
循环:
Sub CSharpBankTest()
On Error GoTo Oops
Dim Out As String
Dim Bank As New BankServerCSharp.Bank 'New!
Dim AllAccounts As BankServerCSharp.AllAccounts 'No New!
Set AllAccounts = Bank.Accounts
Dim AccountOne As BankServerCSharp.Account 'No New
Set AccountOne = AllAccounts.AddAccount
AccountOne.Deposit 47.11
AllAccounts.AddAccount
AllAccounts(2).Deposit 4711
Dim i As Long
Dim ac As BankServerCSharp.Account
For Each ac In AllAccounts
i = i + 1
Out = Out & "Balance of account " & i & ": " & ac.Balance & vbNewLine
Next
Exit Sub
Oops:
MsgBox "Error Message : " & Err.Description, vbOKOnly, "Error"
End Sub