UWP中重用C/C++代碼時踩過的一些坑


標題中提到的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 KitCert就可以。   

現成的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:命名空間和文件名 

運行時組件創建完成時,根命名空間和工程的名稱是一樣的,直接使用沒有問題。但有的同學希望修改命名空間的名稱以符合公司的命名規范,此時,有以下幾點需要注意:   

一個是如何修改命名空間,不僅僅要將代碼中命名空間修改,還要修改工程的根命名空間,方法是打開屬性窗口,單擊工程,就可以看到根命名空間。這兩處要保持致,否則引用的工程無法編譯通過。   

再一個是修改完命名空間后,編譯產生的dllwinmd文件名稱不同,在按文件添加完引用后,主工程可以編譯通過,但是會有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;
    });
}

 

以上是我們在開發過程中遇到的一些坑和解決方案,如有理解不當之處,請小伙伴指出來,另外小伙伴們也可以在評論中積極分享下你們遇到過哪些坑,是如何解決的,大家共同進步。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM