如何将字符串从 C++ 原生传递到 C# Unity?

Posted

技术标签:

【中文标题】如何将字符串从 C++ 原生传递到 C# Unity?【英文标题】:How pass string from C++ native to C# Unity? 【发布时间】:2020-10-15 14:37:08 【问题描述】:

我在 SO 上找到了这样的答案

https://***.com/a/42987032/5709159

还有我的实现

C# 统一

    [DllImport(m_pluginName, CallingConvention = CallingConvention.Cdecl)]
    public static extern IntPtr /* char const * */ get_profiling_info(IntPtr native_stream_ptr);

string foo()
            string prof_info;

            IntPtr pU = get_profiling_info(stream);
            if (pU == IntPtr.Zero)
            
                prof_info = null;
            
            else
            
                prof_info = Marshal.PtrToStringAnsi(pU);
            
    return prof_info;

C++

    DllExport char const * get_profiling_info(void * native_stream_ptr)
    
        TV_DecoderStream * stream_decoder = reinterpret_cast<TV_DecoderStream *>(native_stream_ptr);
        assert(stream_decoder != nullptr);
        std::string result = stream_decoder->m_prof_txt;
        return result.c_str();
    

我得到一个错误

2020-10-15 17:27:42.488 4105-4134/com.com.unityandroidplayer E/AndroidRuntime: FATAL EXCEPTION: UnityMain
    Process: com.com.unityandroidplayer, PID: 4105
    java.lang.Error: *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
    Version '2020.1.8f1 (22e8c0b0c3ec)', Build type 'Development', Scripting Backend 'mono', CPU 'armeabi-v7a'
    Build fingerprint: 'google/taimen/taimen:11/RP1A.201005.004/6782484:user/release-keys'
    Revision: 'rev_10'
    ABI: 'arm'
    Timestamp: 2020-10-15 17:27:41+0300
    pid: 4105, tid: 4134, name: UnityMain  >>> com.com.unityandroidplayer <<<
    uid: 10036
    signal 11 (SIGSEGV), code 2 (SEGV_ACCERR), fault addr 0xbc344000
        r0  9ab64640  r1  bc343ff5  r2  003c11e2  r3  00000034
        r4  be389120  r5  be39cf68  r6  be3b2e10  r7  bc33f9c1
        r8  00000000  r9  0000046c  r10 00000001  r11 bc33f8a0
        ip  a0000000  sp  bc33f880  lr  bc6b8cec  pc  eaecb1d8
    
    backtrace:
          #00 pc 000351d8  /apex/com.android.runtime/lib/bionic/libc.so (__memcpy_base_aligned_a9+144) (BuildId: 92e55abcc7bb795778c1353de856043e)
          #01 pc 001019bf  [anon:stack_and_tls:4134]
    
    managed backtrace:
          #00 (wrapper managed-to-native) object:__icall_wrapper_mono_string_from_bstr_icall (intptr)
          #01 (wrapper managed-to-native) comCAPI:get_profiling_info (intptr)
          #02 comPlayer:OnGUI () <D:\TV_repo\com_unity\TestControlller2\Assets\Scripts\comPlayer.cs:322>
          #03 (wrapper runtime-invoke) object:runtime_invoke_void__this__ (object,intptr,intptr,intptr)
    
        at libc.__memcpy_base_aligned_a9(__memcpy_base_aligned_a9:144)
        at [anon:stack_and_tls:4134].0x1019bf(Native Method)
        at System.Object.__icall_wrapper_mono_string_from_bstr_icall (intptr)(Native Method)
        at comCAPI.get_profiling_info (intptr)(Native Method)
        at comPlayer.OnGUI ()(D:\TV_repo\com_unity\TestControlller2\Assets\Scripts\comPlayer.cs:322)
        at System.Object.runtime_invoke_void__this__ (object,intptr,intptr,intptr)(Native Method)

我做错了什么?

【问题讨论】:

你试过this或this吗? IntPtr pU = get_profiling_info(stream); 中的 stream 是什么? 【参考方案1】:

result 超出范围。然后,从c_str() 返回的指针不再有效。快速解决方法是使用static 字符串:

static std::string result = ...

但更强大的解决方案涉及临时 char 缓冲区的分配/解除分配。或者直接返回m_prof_txt?不过不确定那个变量是什么。

【讨论】:

【参考方案2】:

我已经有一段时间没有进行任何本机/托管互操作了,但我或许可以为您提供帮助。

首先,在您的本机代码中执行以下操作:

#define DLLEXPORT __declspec(dllexport)

extern "C" 
     DLLEXPORT const char* get_profiling_info( void* native_stream_ptr )
    
        TV_DecoderStream * stream_decoder = reinterpret_cast<TV_DecoderStream*>(native_stream_ptr);
        assert(stream_decoder != nullptr);
        std::string result = stream_decoder->m_prof_txt;
        return result.c_str();
    

[DllImport(m_pluginName, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.LPStr)]
public static extern string get_profiling_info( IntPtr native_stream_ptr );

这可能会做到!未经测试,但我很确定这会起作用,除非您有其他问题,我们稍后会解决。您只需要确保您指定了您希望互操作层如何编组事物。我会为你提供一些关于这些东西的官方文档,解释所有细节以及如何使用它......

您会发现 System.Runtime.InteropServices.Marshal 类在托管 本机代码互操作情况下非常有用: System.Runtime.InteropServices.Marshal class 例如,如果 C# 中的公共静态 extern 方法将 IntPtr 作为返回类型(这是可能的),则可以使用 Marshal 类中的方法将该指针转换为有效的 .NET/Mono 字符串对象。你可以用各种各样的东西来做到这一点,一旦你掌握了窍门,你甚至可以开始为你自己的类和结构创建自定义编组规则并来回使用它们。您还可以做一些很酷的事情,例如将 C/C++ 函数指针转换为委托并在 C# 代码中传递它们!我曾经写过一个小的 C# DLL,它可以在运行时加载本地 DLL,从它们获取函数指针,将它们转换为委托,然后在各处使用它们,就像它们被集成到 C# 项目中一样。互操作可能有点棘手,但非常有趣。

另外,阅读 MarshalAsAttribute: MarshalAsAttribute in C#

您可以在此处查看 UnmanagedType 枚举的完整列表: UnmanagedType enum documentation

另一个提示是,在可能的情况下,当我在 C# 和 C++ 代码之间来回传递字符串时,我会尝试使用 std::wstring。 C# 使用 16 位 Unicode 字符,而 std::string 使用标准 ASCII 和 UTF-8 字符,因此它们本身不会相互匹配或映射,并且始终需要显式编组。我认为互操作层实际上可以在不被告知的情况下转换 wchar_t* 和 wstrings,但我对此不是 100% 确定的。我只是阅读或尝试一下,看看会发生什么。


对于互操作,我能想到的另一个重要问题是您(至少您习惯)必须显式地为 C# 程序集选择一个也匹配的目标平台您尝试与之互操作的本机模块,并且它们都必须与目标设备的 CPU 架构兼容。 “任何 CPU”目标平台根本行不通。因此,如果 C++ 代码是为 x86 编译的,则必须为 x86 构建 .NET/Mono,如果一个是 x64,另一个必须是 x64。其他 CPU 架构(ARM、IA-64 等)也是如此。造成这种情况的主要原因之一是因为这些架构可能具有不同的本机指针(地址)宽度和内存模型,可能使用相反的字节序(例如,Big Endian vs Little)并且在它们使用数据的方式上可能存在许多根本差异和记忆。因此,当一些二进制数据试图从托管领域传输到非托管领域(反之亦然)时,互操作层甚至不知道如何处理它或如何编组它。

在这种情况下,您似乎正在尝试在 Android 设备上运行它,并且您的目标是 armeabi-v7a 架构。所以第一步是确保你实际上是在为这个平台编译你的 C++ 代码。我之前没有为 Android 设备编写过任何 C++ 代码(仅 C#/Xamarin),但也可能需要一些额外的依赖项或构建设置。您需要检查文档以确定。在任何情况下,您的 C++ 代码都必须以与您的目标设备/CPU 兼容的方式进行编译,否则它将无法正常工作。并且 C# 程序集很可能仍需要为相同的架构显式编译。

注意:我已经很多年没有在项目中这样做了,因此此时信息可能已经过时,也可能过时。 .NET Framework 现在可能会自动为您执行此操作。但是,我怀疑这仍然是一个重要问题,您需要检查目标平台以在这两个程序集中构建,并确保它们针对相同的 CPU 架构进行编译并具有匹配的指令集。无论如何,让它们匹配目标 CPU 架构并相互匹配不会有什么坏处!

【讨论】:

注意:另外,我没有检查您的 C++ 函数是否正确。我什至不确定您要做什么,但看起来您正试图从内存中的某种设备数据中获取字符串。正如其他人指出的那样,我们无法确定您的变量result 的范围。如果仍然有问题,您应该附加调试器并调查发生了什么。在将result-&gt;cstr(); 返回到 C# 代码之前,您可能需要使用 std::string* result = new std::string(stream_decoder-&gt;m_prof_txt); 在堆上分配它。如果它存在于堆栈中,它就死了。

以上是关于如何将字符串从 C++ 原生传递到 C# Unity?的主要内容,如果未能解决你的问题,请参考以下文章

如何安全地将字符串引用从 c# 传递到 c++?

将数组从非托管 C++ 传递到 C#

Dllimport 将字符串从 C# 传递到 C++

将字符串从 C++ 传递到 C#

如何通过 C++-CLI 回调和委托将字符串从 C++-CLI 传递到 C#

通过互操作将字符串数组从 C# 传递到 C++