天天看點

Win32 C++項目移植到 Win10 UWP

本文可能對誰有幫助

如果你正在做将現有的Win32

靜态庫

DLL

工程移植到Win10

UWP(通用 Windows)

環境,這篇文章可能會對你有幫助。

概述

在VS2015的

建立項目

->

已安裝

->

模闆

->

Visual C++

->

Windows

->

通用

頁面,包含幾個我們需要關心的工程類型:

空白應用(通用 Windows)

DLL(通用 Windwos)

靜态庫(通用 Windows)

Windows 運作時元件(通用 Windows)

根據工程說明可以知道,

DLL(通用 Windwos)

靜态庫(通用 Windows)

可以被

空白應用(通用 Windows)

Windows 運作時元件(通用 Windows)

使用,并且是語言相關的,不能跨語言調用。

Windows 運作時元件(通用 Windows)

可以被

空白應用(通用 Windows)

使用,是語言無關的。也就是說,不管是

C++

還是

C#

開發的應用都可以調用

Windows 運作時元件(通用 Windows)

知道了這一點,那麼我們來看下問題,部落客(C++程式員,未接觸過C#及WinPhone相關開發)遇到的情況是這樣的:将現有的

Win32 平台DLL

移植到

UWP

平台,供采用

C#

開發的Win Phone APP使用,而該

DLL

還依賴其它

C++靜态LIB庫

C動态庫

我們需要做的包括以下幾個方面:

  1. 各類工程到

    UWP

    的轉換
  2. 處理編譯問題
  3. 處理磁盤操作問題
  4. 資料類型間的轉換
  5. 接口封裝問題

開始

首先,請下載下傳Universal Windows Platform (UWP) app samples,将會對你有莫大的幫助。

為友善描述,做如下約定:

  • 被移植的DLL定義為a.dll
  • a.dll依賴的C++靜态LIB庫定義為c++.lib
  • a.dll依賴的C動态庫定義為c.dll
  • 通用 Windows

    版元件加 _rt 字尾以示差別
  • Windows 運作時元件(通用 Windows)外殼定義為 shell_rt.dll

各類工程到

UWP

的轉換

我們整體的工程關系轉換為:
    a.dll  ->    a_rt.lib
  c++.lib  ->  c++_rt.lib
    c.dll  ->    c_rt.lib
  舊的依賴關系:app 依賴a.dll,a.dll 連結c++.lib,a.dll 依賴c.dll;
  新的依賴關系:app 依賴shell_rt.dll,shell_rt.dll 連結a_rt.lib、c++_rt.lib、c_rt.lib,
              并且shell_rt.dll 負責重新封裝a.dll的接口。app 可由 C++ 或 C# 開發。
           
注意: 建立

Windows 運作時元件(通用 Windows)

工程時,必須保證工程内的最外層命名空間名字和最終生成的dll名字(包括winmd檔案)完全一緻,這也是官方的要求。

通過閱讀 官方文檔 得知在不重新建立工程的情況下将現有工程轉換為UWP工程的方法,如下:

  1. 打開 DLL 項目中的“

    項目屬性

    ”,并将“

    配置

    ”設定為“

    所有配置

    ”;
  2. 在“

    項目屬性

    ”中,在“

    C/C++

    ”、“

    正常

    ”頁籤上,将“

    使用 Windows 運作時擴充

    ”設定為“

    是 (/ZW)

    ”。這将啟用元件擴充 (C++/CX);
  3. 在“

    解決方案資料總管

    ”中,選擇項目節點,打開快捷菜單,然後選擇“

    重定SDK版本目标

    ”,“

    确定

    ”;
  4. 在“

    解決方案資料總管

    ”中,選擇項目節點,打開快捷菜單,然後選擇“

    解除安裝項目

    ”。然後,在解除安裝的項目節點上打開快捷菜單,然後選擇編輯項目檔案。找到

    WindowsTargetPlatformVersion

    元素并将其替換為以下元素。然後關閉 .vcxproj 檔案,再次打開快捷菜單,然後選擇“

    重新附加元件目

    ”。現在,解決方案資料總管會将該項目辨別為

    通用 Windows

    項目。
<AppContainerApplication>true</AppContainerApplication>
<ApplicationType>Windows Store</ApplicationType>
<WindowsTargetPlatformVersion>10.0.10156.0</WindowsTargetPlatformVersion>
<WindowsTargetPlatformMinVersion>10.0.10156.0</WindowsTargetPlatformMinVersion>
<ApplicationTypeRevision>10.0</ApplicationTypeRevision>
           

其中, 第3步在官方文檔中沒有,但如果不做第3步,第4步中的

WindowsTargetPlatformVersion

元素可能無法找到。以上涉及的

SDK版本(10.0.10156.0)

,可以根據自己的環境需要進行調整。第2步中打開的“

/ZW

”選項,隻能用于

C++項目

,如果是

C語言

項目的話,需要将該選項設定會

;或者,如果

C++項目

中包含

C檔案

,可以單獨将

C檔案

設定為

處理編譯問題

工程轉換完後開始處理編譯問題。因為不喜歡

stdafx.h

這個檔案名中的

afx

三個字母,部落客一直是把工程的預編譯功能關閉,涉及此檔案的問題這裡不做讨論。

在編譯zlib靜态庫的ARM版本時,遇到了如下編譯問題:

fatal error C1189: #error:  Compiling Desktop applications for the ARM platform is not supported.
           

輕按兩下後可看到以下代碼(

corect.h

),各種宏交錯在一些:

// Verify that the ARM Desktop SDK is available when building an ARM Desktop app
#ifdef _M_ARM
    #if _CRT_BUILD_DESKTOP_APP && !_ARM_WINAPI_PARTITION_DESKTOP_SDK_AVAILABLE
        #error Compiling Desktop applications for the ARM platform is not supported.
    #endif
#endif
           

編譯存在此類問題的代碼檔案頭部增加如下定義,可解決:

#ifdef _M_ARM
    #define WINAPI_FAMILY WINAPI_FAMILY_PHONE_APP
#endif
           

由于編譯問題各式各樣,沒有重點,隻能哪裡編譯不過改哪裡。總之,既然是C++ 程式員,相信你可以搞定。

處理磁盤操作問題

UWP

不支援

fopen

CreateFile

此類操作。用來替換的是

CreateFile2

,用法和

CreateFile

類似。但該

API

隻能處理特殊目錄,例如

程式安裝目錄

圖檔

文檔

視訊

等。對于磁盤中任意的目錄,都沒有操作權限。是以,對于期望可操作任意目錄檔案的需求,隻能放棄使用

CreateFile2

,改用以下

UWP

元件中的磁盤操作類:

Windows::Storage::StorageFile
Windows::Storage::StorageFolder
Windows::Storage::Streams::IRandomAccessStream
           

其它相關類請在類名上按

F12

打開對象浏覽器檢視。

看過類裡的函數之後可以發現大部分函數都有Asyn字尾,帶Asyn字尾的函數均為異步函數,Windows不希望UI線程及其它某些線程因為同步調用導緻響應遲鈍。C++中異步函數的調用方式大緻為:

[代碼片段 A]

// 使用 task 和以下命名空間中的類時需開啟 /ZW 選項,即開啟 C++/CX 支援
 #include <collection.h>
 #include <ppltasks.h>

 using namespace concurrency;
 using namespace Platform;
 using namespace Windows::Storage;
 using namespace Windows::Storage::Streams;

 // 從一個檔案對象擷取其目錄對象 

 void Test(StorageFile ^file)
 {
     create_task(file->GetParentAsync()).then([this, file](StorageFolder ^parentFolder)
     {
         if(parentFolder != nullptr)
         {
             // do something
         }
     });
 }
           

線程A調用

Test

函數,通過

create_task

建立一個

task

對象,并将一個

lamda

函數(位于then()中,[this, file]中聲明的變量可在函數中使用)作為委托傳遞給

task

對象的

then

方法,并繼續向下執行并退出

Test

函數。

task

中的

file->GetParentAsyn()

操作實際由線程B調用,待函數傳回後,再将結果交由線程A執行委托函數

[15-20]行

科普:

^

這個符号讀作

hat

,這裡用來聲明句柄對象。

String ^str;

這裡的

str

就是一個

String

的句柄類型,初始值或無對象指向時為

nullptr

,釋放時可以使用

delete str

也可以讓作用域控制自動釋放。可以簡單的了解為類似智能指針。

異步方法雖然可以避免對線程A的阻塞,但實際使用中并不友善。因為,大部分情況下,我們都會為耗時的網絡或磁盤操作專門開啟線程處理,而不是直接使用UI線程操作。是以如果都使用這種異步方式,在某些場景下,代碼會寫的很反人類,例如下面這個比較完整的檔案讀取操作:

[代碼片段 B]

// 頭檔案、命名空間省略,變量判斷、異常處理省略

 void ReadBytesFromFile(String ^strFilePath)
 {
     // 根據檔案路徑擷取檔案對象; 
     create_task(StorageFile::GetFileFromPathAsync(strFilePath)).then([](StorageFile ^file)
     {
         if (file != nullptr)
         {
             // 以讀寫的方式打開檔案; 
             create_task(file->OpenAsync(FileAccessMode::ReadWrite)).then([](IRandomAccessStream ^stream)
             {
                 if (stream != nullptr)
                 {
                     auto buf = ref new Buffer(); // 讀取10個位元組; 
                     create_task(stream->ReadAsync(buf, , InputStreamOptions::None)).then([buf](IBuffer ^buffer) 
                     {
                         // buf 和 buffer 中包含讀取到的資料; 
                     });
                 }
             });
         }
     });
 }
           

昔日

Win32

的一個

CreateFile

操作,在這裡變的無比繁瑣。而且,上面傳入一個

String

路徑打開檔案的方式因為權限檔案,并不可行。

在系統中,除幾個個别目錄(安裝目錄、圖檔目錄、視訊目錄等)在app配置權限後可用于直接操作權限外,app是無法直接使用任意字元串路徑進行檔案操作的。正确的方式應該是:

  1. 使用

    FolderPicker

    FilePicker

    擷取一個

    StorageFolder

    StorageFile

    對象
  2. 将對象加入到權限清單中

    AccessCache::StorageApplicationPermissions::FutureAccessList->Add(file);

  3. 如果多子產品間傳遞的是String類型,此時可以從

    StorageFolder

    StorageFile

    對象的

    Path

    屬性擷取

    String

    類型路徑字元串,之後可以使用該路徑字元串轉換(見資料類型間的轉換)為

    StorageFolder

    StorageFile

    對象,此時權限仍舊有效。

如果需要在某目錄下建立檔案,則應該使用

FolderPicker

擷取

StorageFolder

對象,将對象加入權限清單,再使用該

StorageFolder

對象建立檔案。

考慮到在做代碼移植時,調整某些線程的同異步模式将會導緻原有架構結構變的混亂,是以,出現了下面的用法:

[代碼片段 C]

// 将file檔案中偏移5開始的10個字元寫入到偏移2開始的位置;
 auto taskOpen = create_task(file->OpenAsync(FileAccessMode::ReadWrite));
 if(taskOpen.wait() == canceled)
     return false;
 IRandomAccessStream ^stream = taskOpen.get();
 stream->Seek();
 auto buffer = ref new Buffer();
 auto taskRead = create_task(stream->ReadAsync(buffer, , InputStreamOptions::None));
 if(taskRead.wait() == canceled)
     return false;

 auto data = ref new Array<byte>(buffer->Length);
 auto reader = DataReader::FromBuffer(buffer);
 reader->ReadBytes(data);

 stream->Seek();
 auto taskWrite = create_task(stream->WriteAsync(buffer));
 if(taskWrite.wait() == canceled)
     return false;
 auto taskFlush = create_task(stream->FlushAsync());
 if(taskFlush.wait() == canceled)
     return false;
           

[12-14]行

隻是示範

IBuffer

對象資料到字元數組資料的轉換,更多的類型轉換見資料類型間的轉換。

注意: 這種

task.wait()

的調用方式并不能應用到所有線程。參見

ppltask.h

檔案的

task_status _Wait();

函數及其中的

_IsNonBlockingThread

函數内部實作。請自行調試實驗各類線程調用

wait()

中的

_IsNonBlockingThread

函數時的傳回情況。

(經過驗證,以上這種寫檔案的寫法效率較低,在頻繁調用時尤為明顯,包括前面列出的對

GetFileFromPathAsync

的調用)

至此,有關磁盤操作的大緻情況如上所述。

資料類型間的轉換

Platform::Array<unsigned char> ^UnsignedChar2Array(unsigned char *pBuffer, unsigned int uSize)
{
    return ref new Platform::Array<unsigned char>(pBuffer, );
}

std::wstring PlatformString2StdWstring(Platform::String ^str)
{
    return std::wstring(str->Data());
}

std::string Unicode2Utf8(Platform::String ^str)
{
    std::wstring wstrTemp(str->Data());

    std::string strUtf8;
    int iUtf8Len = ::WideCharToMultiByte(CP_UTF8, , wstrTemp.c_str(), wstrTemp.length(), NULL, , NULL, NULL);
    if ( == iUtf8Len)
        return "";

    char* pBuf = new char[iUtf8Len + ];
    memset(pBuf, , iUtf8Len + );
    ::WideCharToMultiByte(CP_UTF8, , wstrTemp.c_str(), wstrTemp.length(), pBuf, iUtf8Len, NULL, NULL);

    strUtf8 = pBuf;
    delete[] pBuf;

    return strUtf8;
}

using namespace Windows::Storage::Streams;
IBuffer ^UnsignedChar2Buffer(unsigned char *pBuffer, unsigned int uSize)
{
    DataWriter writer;
    writer.WriteBytes(Platform::ArrayReference<uint8>(pBuffer, uSize));
    return writer.DetachBuffer();
}

void Buffer2UnsignedChar(IBuffer ^buffer, unsigned char **pBuffer, unsigned int *uSize)
{
    DataReader ^reader = DataReader::FromBuffer(buffer);
    *uSize = buffer->Length;
    *pBuffer = new uint8[*uSize];
    reader->ReadBytes(Platform::ArrayReference<uint8>(*pBuffer, *uSize));
}
           

接口封裝問題

UWP 元件的接口不同于Win32 DLL 的導出接口,UWP 的接口是一個winmd 檔案,包含語言無關類型資訊

MetaData

(中繼資料)。使用元件時隻需要 xxx.dll 和 xxx.winmd 兩個檔案,不需要頭檔案。

在導出接口時,首先需要最外層有一個和庫檔案名相同的命名空間名,導出的類需要聲明成如下格式(需帶

public

ref

sealed

聲明 ):

namespace test // 元件名為test.dll
{
    public ref class CInterface sealed
    {
    }
}
           

因為接口可能被跨語言使用,是以下面這種接口參數的寫法就要避免:

void Func(Platform::String ^*pStr);
           

這種寫法隻能被C++使用,如果C#調用的話,會出現崩潰。不過,

Platform::Array<unsigned char>^ *

這種寫法倒是沒有問題。

int

int *

諸如此類,都是可以的,

int *

對于

C#

的調用,使用

out

進行修飾。

// C++方式的接口導出函數聲明
void Func1(int *pParam);
void Func2(const Platform::Array<unsigned char>^ inArray);
void Func3(Platform::Array<unsigned char>^ *outArray);
           
// C#看到的接口聲明
// void Func1(out int pParam);
// void Func2(byte[] inArray);
// void Func3(out byte[] outArray);

// C#方式的接口調用
Int32 param = ;
Func1(out param);

byte[] inArray;
Func2(inArray);

byte[] outArray;
Func3(out outArray);
           

如果接口需要傳遞回調函數,需要封裝成類,可以從接口導出一個

interface

修飾的類:

namespace test
{
    public interface ICallback
    {
    public:
        virtual void func() = ;
    }

    public ref class CInterface sealed
    {
        void RegCallback(ICallback ^callback)
        {
            // 對于callback我做了一層回調封裝映射,由此處的ICallback ^ 類型與内部原有的C++ 回調形成映射關系(中間過渡)
            // 避免C++/CX 文法深入内部
        }
    }
}
           
// C# 使用時
class CCallback : test.ICallback
{
    public void func()
    {
        // do something
    }
}

CCallback callback = new CCallback();
test.CInterface inter = new test.CInterface();
inter.RegCallback(callback);
           

繼續閱讀