Unity与C++交互入门(2)

在介绍复杂数据类型的传递之前,先说一下如何在C++中回调C#函数。

一、delegate与函数指针

Unity与C++交互最麻烦的是调试的过程,在C++ DLL中直接print或cout打印log是没法看到的,我们可以在C++中调用C#的函数来输出log,这需要将delegate映射到C++的函数指针。

在上一节用到的C#脚本中添加如下代码,并在Start()的第一行调用RegisterDebugCallback()。

    void RegisterDebugCallback()
    {
        DebugDelegate callback_delegate = CallBackFunction;
        //将Delegate转换为非托管的函数指针
        IntPtr intptr_delegate = Marshal.GetFunctionPointerForDelegate(callback_delegate);
        //调用非托管函数
        SetDebugFunction(intptr_delegate);
    }

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    public delegate void DebugDelegate(IntPtr strPtr);

    [DllImport("UnityCppInterop")]
    public static extern void SetDebugFunction(IntPtr fp);

    static void CallBackFunction(IntPtr strPtr)
    {
        Debug.LogError("CpppppppppLog: " + Marshal.PtrToStringAnsi(strPtr));
    }

IntPtr代表的是C++的指针,Marshal.GetFunctionPointerForDelegate()的作用是将C#的委托转化为函数指针,通过SetDebugFunction()将回调函数注册到C++中。

在C++项目中新建Debuger类:

//Debuger.h
#pragma once
#include <iostream>
#include <string>
using namespace std;

class Debuger
{
public:

    typedef void(*DebugFuncPtr)(const char *);
    static DebugFuncPtr FuncPtr;
    static void SetDebugFuncPtr(DebugFuncPtr ptr);


    static char container[100];

    static void Log(const string str);
};

//=========================================================================
//Debuger.cpp
#include "Debuger.h"

Debuger::DebugFuncPtr Debuger::FuncPtr;

void Debuger::SetDebugFuncPtr(DebugFuncPtr ptr)
{
    FuncPtr = ptr;
}

char Debuger::container[100];

void Debuger::Log(const string str)
{
    if (FuncPtr != nullptr)
    {
        FuncPtr(str.c_str());
    }
}

修改Bridge类:

//Bridge.h

#ifdef WIN32
#ifdef  UNITY_CPP_INTEROP_DLL_BRIDGE
#define UNITY_CPP_INTEROP_DLL_BRIDGE    __declspec(dllexport)
#else
#define UNITY_CPP_INTEROP_DLL_BRIDGE    __declspec(dllimport)
#endif
#else
// Linux
#define UNITY_CPP_INTEROP_DLL_BRIDGE
#endif

#include <string>
#include <sstream>
#include "Debuger.h"
using namespace std;

extern "C"
{
    UNITY_CPP_INTEROP_DLL_BRIDGE void SetDebugFunction(Debuger::DebugFuncPtr fp);
    UNITY_CPP_INTEROP_DLL_BRIDGE int Internal_Add(int a, int b);
}

//===============================================
//Bridge.cpp
#include "Bridge.h"

extern "C"
{   
    void SetDebugFunction(Debuger::DebugFuncPtr fp)
    {
        Debuger::SetDebugFuncPtr(fp);
    }

    int Internal_Add(int a, int b)
    {
        int res = a + b;
        stringstream ss;
        ss << res;
        Debuger::Log(ss.str());
        return res;
    }
}

运行可以看到输出为:CpppppppppLog: 11 ,说明C++中的log成功打印了。

二、Marshal和Blittable

1.Marshal(封送):指的是将数据从托管内存封送到非托管内存的过程。

2.Blittable和Non-blittable:blittable表示可以被直接复制到非托管内存,而不需要Marshal进行转换处理的数据类型,non-blittable相反。(详见Micorsoft官方文档

Blittable类型包括:

System.Byte
System.SByte
System.Int16
System.UInt16
System.Int32
System.UInt32
System.Int64
System.UInt64
System.IntPtr
System.UIntPtr
System.Single
System.Double
此外,blittable类型的一维数组(如:int[]),以及只包含blittable类型的struct或class(如:struct中只包含int, float等),也属于blittable。

Non-blittable类型包括:

Non-blittable 类型 描述
System.Array 转换为 C 样式数组或 SAFEARRAY
System.Boolean 转换为 1、2 或 4 字节的值,true 表示 1 或 -1。
System.Char 转换为 Unicode 或 ANSI 字符。
System.Class 转换为类接口。
System.Object 转换为变量或接口。
System.Mdarray 转换为 C 样式数组或 SAFEARRAY
System.String 转换为空引用中的终止字符串或转换为 BSTR。
System.Valuetype 转换为具有固定内存布局的结构。
System.Szarray 转换为 C 样式数组或 SAFEARRAY
delegate

C#与C++之间进行数据传递(函数参数、函数返回值等)时,blittable类型可以直接作为参数传递,non-blittable需要借助Marshal做相应转换,但作为函数返回的数据,必须是blittable类型。

三、struct和class的传递

前面提到只包含blittable类型的struct或class也属于blittable类型,因此对于简单的struct和class可以直接传递。

先看个简单的例子,在C#端定义Person结构体,并定义胶水方法ChangePerson()。

//TestCppInterop.cs

[StructLayout(LayoutKind.Sequential)]
public struct Person
{
    public int Age;
    public float Height;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
    public int[] Scores;
    public bool Married;
}

[DllImport("UnityCppInterop", EntryPoint = "Internal_ChangePerson")]
private static extern int ChangePerson(ref Person p);

void Start()
{
    Person p = new Person()
    {
        Age = 18,
        Height = 178.6f,
        Scores = new[] { 66, 77, 88, 99 },
        Married = false
    };
    Debug.LogError("==========Before Cpp Change============");
    Debug.LogError(string.Format("Age: {0}, Height: {1}, Score0: {2}, Score1: {3}, Sex: {4}",       p.Age, p.Height, p.Scores[0], p.Scores[1], p.Married));
    ChangePerson(ref p);
    Debug.LogError("==========After Cpp Change============");
    Debug.LogError(string.Format("Age: {0}, Height: {1}, Score0: {2}, Score1: {3}, Sex: {4}",       p.Age, p.Height, p.Scores[0], p.Scores[1], p.Married));
}

在C++端也要定义同样的结构体Person,和ChangePerson()

//Brige.h
struct Person 
{
public:
    int Age;
    float Height;
    int Scores[4];
    bool Married;
};

extern "C"
{
    UNITY_CPP_INTEROP_DLL_BRIDGE void Internal_ChangePerson(Person* p);
}

//Bridge.cpp
extern "C"
{
    void Internal_ChangePerson(Person* p)
    {
        p->Age += 10;
        p->Height += 10;
        p->Scores[0] += 10;
        p->Scores[1] += 10;
        p->Married = true;
    }
}

输出如下:

ChangePerson.png

StructLayout:传递struct或class时,C#和C++两端的struct映射是按照逐个变量去映射的,但.Net出于优化内存占用的目的,有可能会调整成员变量的排布顺序,而C++编译器不会做这些优化,为了保证两端的struct内存布局一致,需要标记[StructLayout(LayoutKind.Sequential)]特性防止.Net进行优化。

struct内包含数组:此时必须指定数组大小,[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]

在C++中,struct 和 class 除了成员的默认访问权限不一致外,二者基本一样,因此将C#中的struct映射为C++的struct或class都是可以的,C#的class也一样。但在C#中,struct是值类型,class是引用类型,由于C++端使用的是指针,struct类型作为参数传递时,必须使用ref关键字进行引用传递,class则无需ref关键字。

UnityEngine.Vector3:Vector3的成员变量布局顺序是x->y->,在C++中定义相同的Vector3结构体后,便可直接将UnityEngine.Vector3作为参数传递,Quarternion同理。

四、数组的传递

1. 普通数组的传递

传递普通数组很简单,只需要注意同时将数组的长度作为参数传递即可。

//TestCppInterop.cs

[DllImport("UnityCppInterop", EntryPoint = "Internal_ChangeArray")]
private static extern int ChangeArray(int[] array, int size);

void Start()
{        
    int[] numArray = new[] { 1, 2, 3 };
    ChangeArray(numArray, numArray.Length);
    for (var i = 0; i < numArray.Length; i++)
    {
        Debug.LogError(numArray[i]);
    }
}
//Bridge.h
extern "C"
{
    UNITY_CPP_INTEROP_DLL_BRIDGE void Internal_ChangeArray(int* arr, int len);
}

//Bridge.cpp
extern "C"
{
    void Internal_ChangeArray(int* arr, int len)
    {
        for (int i = 0; i < len; i++)
        {
            arr[i]++;
        }
    }
}
ChangeArray.png

2. struct数组的传递

还是用上面的Person,同样需要传递数组长度。

//TestCppInterop.cs

[DllImport("UnityCppInterop", EntryPoint = "Internal_ChangePersonArray")]
private static extern int ChangePersonArray([In, Out]Person[] array, int size);

void Start()
{
    var p1 = new Person(){ Age = 11, Height = 133, Married = false, Scores = new[] { 66, 77} };
    var p2 = new Person() { Age = 22, Height = 177, Married = true, Scores = new[] { 88, 99 } };
    Person[] persons = new Person[2];
    persons[0] = p1;
    persons[1] = p2;
    ChangePersonArray(persons, 2);
    foreach (var person in persons)
    {
        Debug.LogError(person.Age);
        Debug.LogError(person.Height);
        Debug.LogError(person.Married);
        Debug.LogError(person.Scores[0]);
        Debug.LogError(person.Scores[1]);
        Debug.LogError("========================");
    }
}
//Bridge.h
extern "C"
{
    UNITY_CPP_INTEROP_DLL_BRIDGE void Internal_ChangePersonArray(Person* arr, int len);
}

//Bridge.cpp
extern "C"
{
    void Internal_ChangePersonArray(Person* arr, int len)
    {
        Person* curr = arr;
        for (int i = 0; i < len; i++)
        {
            arr->Age += 10;
            arr->Height += 10;
            arr->Married = !arr->Married;
            arr->Scores[0] += 10;
            arr++;
        }
    }
}
ChangePersonArray.png

struct数组作为参数传递时需要使用[In, Out]特性,详见0详见1

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容

  • 这是16年5月份编辑的一份比较杂乱适合自己观看的学习记录文档,今天18年5月份再次想写文章,发现简书还为我保存起的...
    Jenaral阅读 2,731评论 2 9
  • 重新系统学习下C++;但是还是少了好多知识点;socket;unix;stl;boost等; C++ 教程 | 菜...
    kakukeme阅读 19,792评论 0 50
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,678评论 0 9
  • 从小到大,我们都在别人的世界路过,小时候父母就会拿别人家的孩子说,你看人家小明学习好有礼貌,你看人家小红不贪玩爱做...
    如果等于没有阅读 400评论 0 1
  • 自我认知是一个好深奥的话题,我并不是熟人口中所诉的那个人,我并不是家人口述的那个人,我也并不是恋人口中的我。其实都...
    小小蘇的姨阅读 134评论 0 0