標題中提到的UWP,主要是指用C#來寫UWP的主工程,開發過程中可能需要調用C/C++實現的庫。
為什么需要調用C/C++的庫呢,舉個例子,開源庫OpenSSL實現了許多加密算法,穩定快速,我們想在應用中調用;再比如,應用已經在iOS/Android平台上線並穩定運行了,我想把它們的庫拿來給UWP版本用。
經過一些項目的實踐,我總結了下圖所示的幾種集成方式:
圖中紅叉不代表此路不通,只是我們今天不討論P/Invoke方式。我們今天主要通過WinRT來打通C#和C/C++之間的調用,因此,我們的選擇應是前兩種路線。前兩種路線的區別在於,一個直接用原有代碼創建了一個WinRT,另一個是使用WinRT將原DLL的接口進行包裝,方便C#調用。
坑1:代碼移植
下載需要的開源庫,按照默認編譯選項,很輕松得到了dll文件,然后新建一個WinRT,將dll包裝一下,提供給C#使用,簡單幾步就將開源庫集成到UWP中了,但是,在發布到商店時,可恥的失敗了。
由於我們的UWP應用最終是要發布到商店中的,所以必須要能通過商店的認證才可以。開發環境本地就可以檢測x86架構的安裝包是否符合商店要求,使用的工具是Windows App Cert Kit,后面我們簡稱它為WACK,該工具隨Win10 SDK一起安裝。你可以通過小娜快速啟動這個工具,直接在小娜中輸入Windows App Cert Kit或Cert就可以。
現成的dll文件很可能是無法通過商店認證的,快速檢測方法是可以將dll文件作為內容添加到工程中,打包並用WACK驗證,大部分情況下會在支持的API那里驗證不過。 https://msdn.microsoft.com/zh-cn/library/windows/apps/jj606124.aspx 這里列出了部分UWP中不能使用的API。所以,我們需要修改工程或代碼,將這些不能使用的API都替換掉。這是一個非常痛苦的事情,我要一行一行代碼來找哪些API不能用嗎?我要自己修改編譯選項,加各種宏嗎?太可怕了!
一種簡單的做法是這樣的,新建一個Windows通用工程,如下圖所示,這個工程默認是配置好編譯選項和宏的,編出來的庫可以符合商店的認證規則。然后將原代碼添加到工程中,編譯,輸出窗口中會打印出哪些函數是不能用的,我們可以快速定位並進行相應替換或代碼更改。
坑2:引用
如果是直接引用的工程文件,此處可能不會出坑,因為VS會很貼心地將所有必需的文件復制到該去的位置;如果是引用的編譯出來的winmd文件,那么就要注意一些事情了。
我們來看一下編譯輸出的文件,如果是第一種路線,輸出一個winmd和一個dll,將它們放在一起,只需要引用winmd文件就可以了,VS知道編譯的時候帶上dll文件;如果是第二種路線,輸出一個winmd和兩個dll,多出來的dll是動態庫的工程生成的,此時除了引用winmd文件外,還需要將額外的dll文件直接添加到主工程中,同時將文件屬性中的"生成操作"設置為"內容"。
完成上面的引用后,我們還需要在主工程中添加 Visual C++ 2015 Runtime for Universal Windows Platform Apps 和 Microsoft Universal CRT Debug Runtime ,具體添加方式是在工程上右鍵- > 添加 -> 引用 -> Universal Windows -> 擴展。如果不添加,有可能在運行時會收到莫名其妙的找不到文件的崩潰。
最后,提一下如何設置X86/ARM分別引用不同的文件,在添加完對winmd文件的引用后,用記事本打開工程文件,找到剛才引用的winmd文件這里,在路徑中使用$(PlatformTarget)來代替X86/X64/ARM,使用$(Configuration)來代替Debug/Release,這樣,VS在編譯的時候就可以為不同的目標使用不同的庫文件了。
坑3:命名空間和文件名
運行時組件創建完成時,根命名空間和工程的名稱是一樣的,直接使用沒有問題。但有的同學希望修改命名空間的名稱以符合公司的命名規范,此時,有以下幾點需要注意:
一個是如何修改命名空間,不僅僅要將代碼中命名空間修改,還要修改工程的根命名空間,方法是打開屬性窗口,單擊工程,就可以看到根命名空間。這兩處要保持致,否則引用的工程無法編譯通過。
再一個是修改完命名空間后,編譯產生的dll和winmd文件名稱不同,在按文件添加完引用后,主工程可以編譯通過,但是會有APPX1707的警告,並且運行時會發生崩潰。分析后發現,原來是VS默認引用的時候會用winmd文件的名稱去找dll文件,而現在兩個文件名稱不一樣,所以會找不到實現。解決方案有兩種,一種是修改運行時組件工程選項,使得輸出的文件名保持一致;另一種是在主工程添加完winmd文件的引用后,手動編輯一下主工程的工程文件,在引用處加入如下圖所示的一行,強制指定對應的實現在哪個dll文件中。
坑4:字符串轉換
C/C++實現的庫中,有相當一部分還是用的std::string,沒有使用寬字符,為了保證這種情況下的中文可以正確的傳遞不出亂碼,需要對字符串進行轉換。閑話不多說,直接上代碼。
// UTF編碼的多字節轉為寬字符 std::wstring TUtf8ToUnicode(const char * pszUtf8Str, unsigned len = -1) { std::wstring ret; do { if (!pszUtf8Str) break; // get UTF8 string length if (-1 == len) { len = strlen(pszUtf8Str); } if (len <= 0) break; // get UTF16 string length int wLen = MultiByteToWideChar(CP_UTF8, 0, pszUtf8Str, len, 0, 0); if (0 == wLen || 0xFFFD == wLen) break; // convert string wchar_t * pwszStr = new(std::nothrow) wchar_t[wLen + 1]; if (!pwszStr) break; pwszStr[wLen] = 0; MultiByteToWideChar(CP_UTF8, 0, pszUtf8Str, len, pwszStr, wLen + 1); ret = pwszStr; delete[] pwszStr; } while (0); return ret; } // std::string => Platform::String Platform::String ^ Ts2ps(std::string str) { return ref new Platform::String(TUtf8ToUnicode(str.c_str()).c_str()); } // 寬字符轉為UTF8編碼的多字節 std::string TUnicodeToUtf8(const wchar_t* pwszStr) { std::string ret; do { if (!pwszStr) break; size_t len = wcslen(pwszStr); if (len <= 0) break; size_t convertedChars = 0; char * pszUtf8Str = new(std::nothrow) char[len * 3 + 1]; if (!pszUtf8Str) break; WideCharToMultiByte(CP_UTF8, 0, pwszStr, len + 1, pszUtf8Str, len * 3 + 1, 0, 0); ret = pszUtf8Str; delete[] pszUtf8Str; } while (0); return ret; } // Platform::String => std::string std::string Tps2s(Platform::String ^ pstr) { if (pstr == nullptr) return ""; return TUnicodeToUtf8(pstr->Data()); }
坑5:數組在異步中怎么傳
如何在接口中使用數組,可以參考 https://msdn.microsoft.com/zh-cn/library/hh700131.aspx 這篇文章。里面提到不同場景下分別用什么樣的數組,講的非常詳細,這里就不再重復了,重點扒一下遇到的坑。
數組作為WinRT接口的返回值時,如果該接口是同步的,一切正常;當接口是異步的時候,編譯器就會拋出
error C3952: 'Platform::Array<int,1> ': WinRT does not support 'in/out' arrays. Use 'const Array<T>^' for 'in' and 'WriteOnlyArray<T>' or 'Array<T>^*' for 'out' on public APIs
按照該提示進行修改,怎么都不好使,后經過多方求解,才知道這種情況下需要進行裝箱操作才可以。示例代碼出下:
IAsyncOperation<Platform::Object^>^ GetArrayAsync() { return create_async([]() -> Platform::Object^ { Array<int>^ arr1 = ref new Array<int>(10); for (int i = 0; i < arr1->Length; i++) { arr1[i] = i + 1; } Platform::Object^ a = arr1; return a; }); }
以上是我們在開發過程中遇到的一些坑和解決方案,如有理解不當之處,請小伙伴指出來,另外小伙伴們也可以在評論中積極分享下你們遇到過哪些坑,是如何解決的,大家共同進步。