前言
术业有专攻,当然编程语言也是这样。而不同的应用场景下单一语言或者并不能满足需求。比如:一些性能要求高的模块可以用CPP实现。常见的如游戏开发引擎,譬如Unity底层使用CPP开发,上层使用C#包装,充分利用两种语言的优势。又比如一些已有的CPP库没有对应语言的库供使用,那么互操作的重要性就体现出来了。
C#与CPP互操作的方式
P/Invoke 是可用于从托管代码访问非托管库中的结构、回调和函数的一种技术。它允许托管代码调用非托管代码。对于C#通过P/Invoke可以很方便的实现对CPP方法的调用。但是对于CPP中比较复杂的数据类型时,操作比较繁琐。
C++/CLI 是微软对C++语言做的拓展,允许C++与托管语言的相互操作。非常适合处理非常复杂的数据类型和对象。但语法比较复杂,写起来比较难受,并且只支持windows平台。
比较新的方案:Mono/CoreCLR。没自己绑定多,后续研究了再补充。
COM互操作,C++/WinRT 没用过,也比较老了。
一个简单的例子
下面是将cocos2dx中Node的getTag/setTag
方法导出的一个实例。
- 定义一个方法,方法名最好与CPP中保持一致(当然也可以在DllImport特性中指定),参数与CPP中保持一致。
- 使用
extern
标记此方法,告诉编译器此方法在Native中实现。因此也不需要写方法体。
- 给当前方法加一个
DllImport
特性,并制定从哪里导入,和调用约定。
DllImport 的一些常用参数如下:
参数 |
解释 |
CallingConvention |
指定调用约定,调用方需要与被调用方保持一致,如:Cdecl、StdCall |
EntryPoint |
导入的方法名 |
CharSet |
指定字符集,确保非托管字符串到非托管字符串不会乱码,如:CharSet.Ansi、CharSet.Unicode |
SetLastError |
约定调用失败了获取系统反回的错误码 |
1 2 3 4 5
| [DllImport("xgame.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "getTag")] public static extern int GetTag(IntPtr node); [DllImport(""xgame.dll"", CallingConvention = CallingConvention.Cdecl, EntryPoint = "setTag")] public static extern void SetTag(IntPtr node, int tag);
|
那么C++中的导出方法该如何实现呢?
extern "C"
约定使用C语言的方法生成方法名,不然导出的方法名乱七八糟,可以自行使用dumpbin查看。
- 使用
__declspec(dllexport)
标记需要导出的方法。
1 2 3 4 5 6 7 8 9 10 11
| extern "C" { __declspec(dllexport) int getTag(void* obj){ const cocos2d::Node* node = static_cast<cocos2d::Node*>(obj); return node->getTag(); } __declspec(dllexport) void setTag(void* obj, int tag){ cocos2d::Node* node = static_cast<cocos2d::Node*>(obj); node->setTag(tag); } }
|
这个例子展示了int参数的传递,其他的数据类型对应如下:
C# |
CPP |
int/float/double |
int/float/double |
sbyte |
char |
string |
char* |
IntPtr |
void* |
bool |
bool |
string类型的传递
1 2 3 4 5 6 7 8 9 10 11 12 13
| extern "C" { __declspec(dllexport) const char* getName(void* obj){ const cocos2d::Node* node = static_cast<cocos2d::Node*>(obj); const char* name = node->getName().c_str(); return name; } __declspec(dllexport) void setName(void* obj, const char* name){ cocos2d::Node* node = static_cast<cocos2d::Node*>(nodeObj); node->setName(name); } }
|
1 2 3 4 5 6 7 8
| [DllImport("xgame.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi, EntryPoint = "getName")] private static extern IntPtr getNameNative(IntPtr nodePtr); [DllImport("xgame.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "setName")] private static extern void setNameNative(IntPtr nodePtr, [MarshalAs(UnmanagedType.LPStr)] string name);
public static string GetName(IntPtr node) { return Marshal.PtrToStringAnsi(getNameNative(node)); } public static void SetName(IntPtr node, string value) { setNameNative(node, value); }
|
对象数组的传递
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| extern "C" { struct Cocos2dNodeArray { cocos2d::Node** data; size_t size; }; __declspec(dllexport) Cocos2dNodeArray getChildren(cocos2d::Node* node) { const cocos2d::Vector<cocos2d::Node*>& array = node->getChildren(); const ssize_t count = node->getChildrenCount(); Cocos2dNodeArray result; result.size = count; result.data = (cocos2d::Node**)malloc(count * sizeof(cocos2d::Node*)); for (size_t i = 0; i < count; i++) { result.data[i] = array.at(i); } return result; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| [DllImport("xgame.dll", CallingConvention = CallingConvention.Cdecl)] private static extern Cocos2dNodeArray getChildren(IntPtr node);
[StructLayout(LayoutKind.Sequential)] public struct Cocos2dNodeArray { public IntPtr data; public int count; }
public static IntPtr[] GetChildren(IntPtr node) { Cocos2dNodeArray array = getChildren(node); int count = array.count;
IntPtr[] ptrArray = new IntPtr[array.count]; Marshal.Copy(array.data, ptrArray, 0, array.count); return ptrArray; }
|
C#传递方法委托到CPP
这里是给cocos2dx的所有节点注入下C#的回调
1 2 3 4 5 6 7 8
| [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate void OnCtorCallback(IntPtr parent, IntPtr child);
[DllImport("xgame.dll", CallingConvention = CallingConvention.Cdecl,EntryPoint = "setCtorCallback")] public static extern void SetCtorCallback(OnCtorCallback ctor);
CCNativeNode.SetCtorCallback((parentPtr, childPtr) => { });
|
CPP 中使用委托
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| typedef void(*CtorCallback)(void* parent, void* child); extern "C" { __declspec(dllexport) void setCtorCallback(CtorCallback callback) { Node::setCSharpCtorCallback([callback](Node* parent, Node* child) {if (callback) callback(parent, child); }); } }
static std::function<void(Node*, Node*)> _onCSharpCtorCallback;
static void setCSharpCtorCallback(const std::function<void(Node*, Node*)>& callback) { _onCSharpCtorCallback = callback; }
std::function<void(Node*, Node*)> Node::_onCSharpCtorCallback = nullptr;
Node * Node::create() { Node * ret = new (std::nothrow) Node(); #if CC_BIND_CSHARP if (Node::_onCSharpCtorCallback) Node::_onCSharpCtorCallback(ret->_parent, ret); #endif return ret; }
|
其他
为啥要用C语言的方式导出?
本地人不小心加了命名空间,导致C#一直找不到native 方法,被坑了,通过dumpbin查看发现CPP导出的方法名比较乱,如下。
顺便记录下查看dll的所有导出方法:dumpbin \EXPORTS dll_path
1 2 3 4 5 6
| // 不约定的如下: 487 1E6 00472812 ?clone@Waves3D@cocos2d@@QBEPAVGrid3DAction@2@XZ = @ILT+18445(?clone@Waves3D@cocos2d@@QBEPAVGrid3DAction@2@XZ)
// 约定C语言的如下: 525 20C 0048AA8E getName = @ILT+117385(_getName) 542 21D 00478C0D setName = @ILT+44040(_setName)
|