zoukankan      html  css  js  c++  java
  • 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;
        });
    }

    以上是我们在开发过程中遇到的一些坑和解决方案,如有理解不当之处,请小伙伴指出来,另外小伙伴们也可以在评论中积极分享下你们遇到过哪些坑,是如何解决的,大家共同进步。

  • 相关阅读:
    欧拉计划之题目7:找出第10001个质数
    DShow实现一个avi视频的播放(含有个人解释和注释)
    DirectX 9 SDK安装后在vs2010里编译BaseClasses出错问题解决方法
    C#内存占用大量资源的解决办法
    C#读写ini文件操作
    【Java】编程技术经典书籍列表
    【数据库_Mysql】查询当前年份的sql
    【数据库_Mysql】MySQL动态语句 if set choose where foreach trim
    【JavaScript】20款漂亮的css字体
    【数据库_Mysql】<foreach>标签在Mysql中的使用
  • 原文地址:https://www.cnblogs.com/ms-uap/p/5545621.html
Copyright © 2011-2022 走看看