前一章,我们讨论了Interop繁杂和版本难以追踪的问题,这一章我们讲解如何合并多个Interop以及如何增加命名规则。
Interop合并方案
利器出窍
要想实现Interop合并,我之前也试过一些方法,比如把Interop全部反编译成C#,然后合并到一个类库项目里面。但是这些尝试最终都以失败告终。原因就是反编译质量达不到要求,代码需要改动的地方太多了,就下图这些DLL反编译出来,代码错误量就达到了7000多处,根本改不起。后来我就Google了一下,看看有没有工具可以把编译好的DLL文件直接合并的。然后就发现了这个利器ILMerge。
这个工具可以说特别适合Interop的合并,下图是使用界面,把你想合并的Interop拖进去,然后按合并按钮,过程中会有些错误要处理,主要就是Interop文件会引用其他的Interop,所以必须把所有你需要合并的和关联的Interop都放在一个目录下,这样他才能在合并以后,仍然是一个正常的DLL文件。
合并的基本原则
比如我有A.dll B.dll 还有C.dll,其中B和C都引用了A,现在我想把BC合并成BC.dll,那么你就需要把三个文件放在一个目录下面,然后拖拽BC两个文件到ILMerge中,然后合并生成BC.dll。项目交付的时候你就需要交付A.dll和BC.dll两个Dll就好了。
当然你也可以,直接合并出来一个叫做ABC.dll文件,这样发版的时候就一个文件了,甚至你可以把你的exe和ABC.dll也合并到一起,最后就剩下一个exe文件。
但是要注意,最好不要把U8原厂的.NET的DLL文件合并,因为原厂的DLL会有强命名,你合并以后没法调用其他的类库了。这件事情说起来比较绕口,只需记住几个基本原则:
- 凡是Interop类型的或者AxInterop类型的文件,全部可以合并到一个DLL中。
- 凡是U8原厂的非Interop(包括AxInterop)文件,一律不进行合并。
- 自己写的类库,可以合并,但是不建议合并到Interop的那个DLL中,可以考虑直接和exe合并。
合并前,有一点需要特别注意,就是这些待合并的Interop文件,一定都是经过修复引用的,千万不要有DLL陷阱等问题,否则就会失败。
还有一点需要说明一下,这个工具只能合并出来Framework4.0的DLL,如果觉得太高,可以使用DotNetHelper转成3.5的。
合并后类名转换
在理想小节中讲过,为了防止类名同名,我希望能够将所有的Interop类名标记成带有版本号的,这样在我开发的时候和编译的时候能够准确知道是否是我想要的版本,代码和DLL是否能匹配上。
所以这里介绍如何将合并后的Interop文件中的类名重命名。由于这里没有工具可以使用,所以我纠结了很长时间是否需要改名,有一段时间甚至想要放弃这个想法,直到我看到了Mono.cecil类库。
其实修改类名的工作在IL级别上可以完成,但是经过实际的修改发现,工作量简直了!所以当需要批量有规则的修改IL的时候最好使用Mono.cecil类库进行编程。
改名前,先看一下我们合并出来的类库到底什么样子,然后修改后应该是什么样子。
上图就是修改后和修改前的对比,我主要把第一级的命名空间改变了,例如VBA.Collection这个类变成了COMV13000_VBA.Collection。
有了清晰的目标,我们就可以把大象放进冰箱里面了,正好三步。
修改步骤1 准备文件
下面说一下具体的操作环境和代码,首先需要把合并好的Interop DLL保存好,这里刚刚合并好的文件名称是U8Lib_V13000.dll,把他放在一个固定的文件夹下。我放入了C:开发目录MiniU8MegerU8Lib_V13000U8Lib_V13000.dll。
修改步骤2 执行代码
using Mono.Cecil;
static void Main(string[] args)
{
//这里增加修改的类名对照
var dicRename = new Dictionary<string, string>();
dicRename["U8Login"] = "COMV1300_U8Login";
dicRename["ADODB"] = "COMV1300_ADODB";
dicRename["interop.userpco"] = "COMV1300_USERPCO";
dicRename["Interop.VoucherCO_SA"] = "COMV1300_VoucherCO_SA";
dicRename["MSXML2"] = "COMV1300_MSXML2";
dicRename["Scripting"] = "COMV1300_Scripting";
dicRename["USCOMMON"] = "COMV1300_USCOMMON";
dicRename["USERPVO"] = "COMV1300_USERPVO";
dicRename["USSAServer"] = "COMV1300_USSAServer";
dicRename["VBA"] = "COMV1300_VBA";
dicRename["MultiLangPkg"] = "COMV1300_MultiLangPkg";
dicRename["SystemInfo"] = "COMV1300_SystemInfo";
dicRename["AxUAPVoucherControl85"] = "COMV1300_AxUAPVoucherControl85";
dicRename["Skinse_VB_API"] = "COMV1300_Skinse_VB_API";
dicRename["UAPUfToolKit85"] = "COMV1300_UAPUfToolKit85";
dicRename["UAPVoucherControl85"] = "COMV1300_UAPVoucherControl85";
dicRename["UFVoucherServer85"] = "COMV1300_UFVoucherServer85";
//读取文件
var assembly = AssemblyDefinition.ReadAssembly("C:\开发目录\MiniU8Meger\U8Lib_V13000\U8Lib_V13000.dll");
;
//修改文件
ModifyAssemblyByDic(dicRename,ref assembly);
//保存文件
assembly.Write("C:\开发目录\MiniU8Meger\U8Lib_V13000\U8Lib.dll");
}
private static void ModifyAssemblyByDic(Dictionary<string, string> dicRename, ref AssemblyDefinition assembly)
{
var types = assembly.MainModule.Types;
assembly.Name.Name = "U8Lib";
foreach (var type in types)
{
Console.WriteLine($"typename:{type.Name} namespace:{type.Namespace} ");
ReplaceNameByDic(type, dicRename);
try
{
var attrs =
type.CustomAttributes.Where(ca => ca.AttributeType.FullName.Contains("CoClassAttribut"));
foreach (var attr in attrs)
{
if (attr.ConstructorArguments[0].Value.GetType().Equals( typeof(TypeDefinition)))
{
var c = (TypeDefinition)attr.ConstructorArguments[0].Value;
ReplaceNameByDic(c, dicRename);
}
else if (attr.ConstructorArguments[0].Value.GetType().Equals(typeof(TypeReference)))
{
var r = (TypeReference)attr.ConstructorArguments[0].Value;
ReplaceNameByDic(r, dicRename);
}
else
{
}
}
attrs =
type.CustomAttributes.Where(ca => ca.AttributeType.FullName.Contains("ComEventInterfaceAttribut"));
foreach (var attr in attrs)
{
foreach (var aa in attr.ConstructorArguments)
{
var c = (TypeDefinition)aa.Value;
ReplaceNameByDic(c, dicRename);
}
}
attrs = type.CustomAttributes.Where(ca => ca.AttributeType.FullName.Contains("ComSourceInterfacesAttribut"));
foreach (var attr in attrs)
{
foreach (var aa in attr.ConstructorArguments)
{
var c = (TypeDefinition)aa.Value;
ReplaceNameByDic(c, dicRename);
}
}
}
catch (Exception ex)
{
// ignored
}
}
}
private static void ReplaceNameByDic(TypeReference type, IDictionary<string, string> dicRename)
{
var key = type.Namespace;
if (dicRename.ContainsKey(key))
type.Namespace = dicRename[key];
}
修改步骤3 测试成果
到此成果物出来了,就是C:开发目录MiniU8MegerU8Lib_V13000U8Lib.dll,我们可以引用到C#项目里面看看效果了。
注意事项
在ILmerge的时候一定不能选第二个选项,就是上面截图中的Union duplicate,否则出来的文件非常奇怪,用Cecil我没有改明白。如果哪位仁兄研究出来,可以跟我讲讲,反正我的代码不能修改Union duplicate合并出来的DLL。修改出来也不能正常使用。
使用技巧
1 编译环境自由
平时使用的时候,直接引用U8Lib.dll,然后就可以正常写代码了,甚至你都可以在没有U8安装的环境里面编译代码,只要U8Lib.dll在,所有的COM都可以正常编译和使用,但是这里有一点需要注意,具有界面的控件,可以正常编译,但是在设计界面的时候,会出错。下图是正常情况下的,不正常的我就不演示了。
2 可以嵌入程序中
在引用U8Lib时,有个叫做是否嵌入的选项,如果选择是,VS会在编译的时候直接将U8Lib中的类嵌入到exe文件中或者你的类库中,发版时就不需要U8Lib了,但是我不建议这样使用,因为当U8Lib中含有Axinterop或者说有控件类的COM,VS编译时,强行不嵌入。原因不明。如果你的项目全程无界面,就是个WebService可以考虑使用嵌入。
结束
到此C#调用U8 COM组件技术的介绍,就全部结束了。该技术目前可以稳定运行在Framework4.0环境中,换言之,你可以用4.0开发U8的相关组件,生产环境中,我有个WebService项目就是4.0的,提供给第三方供应商api功能,方便对接调用,他调我的WebService,我调Interop的COM保存单据,从订单发货到出库,从采购入库到发票,全部自动生成,自动咬合,用起来还是蛮舒服的,这样不用花钱买eai了,爽哉,哈哈。
后期我会考虑,传送一部分成果物到百度网盘上,给大家看看成果物是什么样子的,但是目前代码比较乱,新旧技术混杂在一起,所以我再整理一下