前一章,我們討論了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:\開發目錄\MiniU8Meger\U8Lib_V13000\U8Lib_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:\開發目錄\MiniU8Meger\U8Lib_V13000\U8Lib.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了,爽哉,哈哈。
后期我會考慮,傳送一部分成果物到百度網盤上,給大家看看成果物是什么樣子的,但是目前代碼比較亂,新舊技術混雜在一起,所以我再整理一下