本帖最后由 Seekladoom 于 2021-8-12 14:58 编辑  
 
来源链接:https://blog.csdn.net/conan98/article/details/52095938 
 
 
本文可能对谁有帮助 
如果你正在做将现有的Win32 静态库 或 DLL 工程移植到Win10 UWP(通用 Windows) 环境,这篇文章可能会对你有帮助。 
补充:本文虽然是针对WIN10的UWP平台准备的移植解决方案,但其中的方法和思想依然可以用在WIN7、WIN8、WIN11的软件开发上。 
 
 
概述 
在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,将会对你有莫大的帮助。 
为方便描述,做如下约定:1.被移植的DLL定义为a.dll 
2.a.dll依赖的C++静态LIB库定义为c++.lib 
3.a.dll依赖的C动态库定义为c.dll 
4.通用 Windows版组件加 _rt 后缀以示区别 
5.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); // 读取10个字节; 
 
 -                      create_task(stream->ReadAsync(buf, 10, 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(5);
 
 -  auto buffer = ref new Buffer(10);
 
 -  auto taskRead = create_task(stream->ReadAsync(buffer, 10, 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(2);
 
 -  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, 10);
 
 - }
 
  
- 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, 0, wstrTemp.c_str(), wstrTemp.length(), NULL, 0, NULL, NULL);
 
 -     if (0 == iUtf8Len)
 
 -         return "";
 
  
-     char* pBuf = new char[iUtf8Len + 1];
 
 -     memset(pBuf, 0, iUtf8Len + 1);
 
 -     ::WideCharToMultiByte(CP_UTF8, 0, 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 = 0;
 
 - Func1(out param);
 
  
- byte[] inArray;
 
 - Func2(inArray);
 
  
- byte[] outArray;
 
 - Func3(out outArray);
 
  复制代码 如果接口需要传递回调函数,需要封装成类,可以从接口导出一个interface 修饰的类: 
- namespace test
 
 - {
 
 -     public interface ICallback
 
 -     {
 
 -     public:
 
 -         virtual void func() = 0;
 
 -     }
 
  
-     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);
 
  复制代码 
 
 
 |