本文可能對誰有幫助
如果你正在做将現有的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動态庫
。
我們需要做的包括以下幾個方面:
- 各類工程到
的轉換UWP
- 處理編譯問題
- 處理磁盤操作問題
- 資料類型間的轉換
- 接口封裝問題
開始
首先,請下載下傳Universal Windows Platform (UWP) app samples,将會對你有莫大的幫助。
為友善描述,做如下約定:
- 被移植的DLL定義為a.dll
- a.dll依賴的C++靜态LIB庫定義為c++.lib
- a.dll依賴的C動态庫定義為c.dll
-
版元件加 _rt 字尾以示差別通用 Windows
- Windows 運作時元件(通用 Windows)外殼定義為 shell_rt.dll
各類工程到 UWP
的轉換
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工程的方法,如下:
- 打開 DLL 項目中的“
”,并将“項目屬性
”設定為“配置
”;所有配置
- 在“
”中,在“項目屬性
”、“C/C++
”頁籤上,将“正常
”設定為“使用 Windows 運作時擴充
”。這将啟用元件擴充 (C++/CX);是 (/ZW)
- 在“
”中,選擇項目節點,打開快捷菜單,然後選擇“解決方案資料總管
”,“重定SDK版本目标
”;确定
- 在“
”中,選擇項目節點,打開快捷菜單,然後選擇“解決方案資料總管
”。然後,在解除安裝的項目節點上打開快捷菜單,然後選擇編輯項目檔案。找到解除安裝項目
元素并将其替換為以下元素。然後關閉 .vcxproj 檔案,再次打開快捷菜單,然後選擇“WindowsTargetPlatformVersion
”。現在,解決方案資料總管會将該項目辨別為重新附加元件目
項目。通用 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是無法直接使用任意字元串路徑進行檔案操作的。正确的方式應該是:
- 使用
或FolderPicker
擷取一個FilePicker
或StorageFolder
對象StorageFile
- 将對象加入到權限清單中
AccessCache::StorageApplicationPermissions::FutureAccessList->Add(file);
- 如果多子產品間傳遞的是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);