Unity 与 C++
C#是Unity的官方推荐的开发语言。如果某些逻辑需要C++支持以提供高性能特性,或者需要C++去跟底层硬件或者操作系统级别的接口交互,那么就需要用C#去调用C++的接口。
这往往依赖动态链接。即用C++开发动态链接库,然后C#调用动态链接库里暴露的接口。
而在Android上开发动态链接库(.so文件)的方法,就是NDK开发。
NDK开发动态链接库
NDK
NDK 全称原生开发工具包(Native Development Kit),是Google提供的在安卓上直接使用C++/C之类的底层语言进行原生开发的方式。原生开发意味着你的代码不会产生类似java字节码那样需要被虚拟机代理解释的代码,而是直接被编译成本机处理器能够识别的机器码。为此你需要先下载这整个工具包:下载地址
注意需要下载的版本。如果你的NDK同时也为Unity的IL2cpp服务,那就需要注意Unity对NDK的版本有硬性要求,比如2018的版本要求 NDK r16b的版本(我不清楚如果仅仅用作NDK开发,用其他版本的NDK会不会有影响)。
下载下来的压缩包,解压之后会得到如下目录:
最重要的是那个叫做 ndk-build.cmd 的脚本文件。它是一个基于make的构建脚本。查看它的源码你会发现它实际上执行的是:
make.exe -f ./build/core/build-local.mk SHELL=cmd ...
其中,build-local.mk可以在上述的路径中找到,...
表示从命令行中传过来的所有参数。为了能够在全局使用这个脚本,你最好将这个目录加入到环境变量中。
利用NDK构建.so
现在开始利用NDK构建.so文件,目的是在.so文件中暴露一个函数给C#调用。这个函数是自定义的加法函数,它会将两个整型相加,然后乘以10,再返回结果。新建一个 main.cpp 和 main.h。内容分别如下:
// main.cpp
extern "C"
{
int MyAdd(int a, int b)
{
return (a+b)*10;
}
}
// main.h
extern "C"
{
__declspec(dllexport) int MyAdd(int a, int b);
}
上面代码的关键点有:
- extern “C”:指示编译器将这段代码按照C语言的方式进行编译。由于C++支持重载的缘故,C++的编译器编译代码之后,其函数的名称与其在符号表中对应的符号名一般是不相等的。
- __declspec(dllexport):在动态链接库中,需要导出的符号(变量,函数,结构体,等)都需要使用这个修饰前缀。
我们将在这两个文件所在的目录下运行ndk-build.cmd
,在此之前,我们还需要两个文件:Android.mk 和 Application.mk。这两个文件将包含 ndk-build 所需要的所有配置。两个文件的内容如下:
// Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := NDKTest
LOCAL_C_INCLUDES := $(LOCAL_PATH)
LOCAL_SRC_FILES := main.cpp
LOCAL_CFLAGS := -DANDROID_NDK
include $(BUILD_SHARED_LIBRARY)
// Application.mk
APP_STL := gnustl_static
APP_CPPFLAGS := -frtti -std=c++11
APP_PLATFORM := android-19
APP_CFLAGS += -Wno-error=format-security
APP_BUILD_SCRIPT := Android.mk
APP_ABI := armeabi-v7a,,arm64-v8a
Android.mk 为 ndk-build描述了构建所需的源代码,链接库等信息。
变量 | 描述 |
---|---|
LOCAL_PATH | 这个变量描述了构建所需的源代码在当前工程中的相对位置。如果你是使用Android Studio进行开发,那源代码一般位于工程的 app/src/main/cpp 目录下。$(call my-dir) 返回Android.mk所在的目录。 |
CLEAR_VARS | 将一个特殊的Makefile文件包含进来。CLEAR_VARS 定义了这个文件的位置。这个Makefile文件会清除所有之前定义过的Android.mk变量,一般都以LOCAL_ 开头 |
LOCAL_MODULE | ndk-build构建出来的库的名称,如果设定的名称没有以lib开头,则ndk-build会自动为你加这个前缀。例如,上面定义的名称将构建出来一个叫libNDKTest.so 的库 |
LOCAL_C_INCLUDES | 当你的代码中include一些文件不在默认的搜索路径时,你可以使用这个变量去指定。一般来说,标准c头文件,和一些常见的Android头文件,都是在默认的搜索路径中的,他们都可以在你下载的NDK工具包中找得到 |
LOCAL_SRC_FILES | 需要构建的源文件(不包含头文件)都需要放到这里面 |
LOCAL_CFLAGS | 这个变量定义一些ndk-build的编译选项,一般来说,不需要额外的设置 |
BUILD_SHARED_LIBRARY | 将一个特殊的构建文件包含进来。BUILD_SHARED_LIBRARY 定义了这个文件的位置。该文件会收集你在定义的所有上述LOCAL_ 开头的变量所包含的信息,然后确定如何从源文件中构建动态链接库。这个构建脚本要求你至少定义了LOCAL_MODULE 和LOCAL_SRC_FILES 两个变量。 |
Application.mk 为ndk-build描述了项目的构建性质。
变量 | 描述 |
---|---|
APP_STL | 需要使用哪些C++标准库 |
APP_CPPFLAGS | 项目中,C++部分使用的编译选项 |
APP_PLATFORM | 构建的Android API级别,对应于应用程序的min SDK Version |
APP_CFLAGS | 项目中,C/C++部分使用的编译选项 |
APP_BUILD_SCRIPT | Android.mk 文件所在的位置 |
APP_ABI | 需要为哪些abi机器构建共享库 |
然后我们在终端输入以下命令:
ndk-build.cmd NDK_PROJECT_PATH=. NDK_APPLICATION_MK=Application.mk
前者指定ndk-build的工程的根目录,后者指定配置文件的位置。输出如下:
你会得到一个libs目录和一个obj中间文件暂存目录。libs目录里面就是对应你所设置的abi平台对应的共享库
在Unity中使用.so
只有将.so文件放在/Assets/Plugins/Android/libs下,Unity才会将.so文件识别为共享库,并在打包时将之拷贝到合适的地方。
将我们打包出来的.so文件放到上述的目录下。因为不同abi的共享库名称都一样,为避免冲突,我们拷贝它的任意父目录即可。无论.so存放到哪儿,Unity都会识别出这个共享库属于哪个abi平台的。项目目录结构如下:
在场景中新建一个空物体GameMgr,为其挂上一个脚本GameMgr.cs,脚本加载动态链接库,引用函数,计算MyAdd(233, 2)的结果,然后在一个Text UI上显示出结果(记得加上平台判定宏,IOS不支持装载动态链接库):
using System.Runtime.InteropServices;
using UnityEngine;
using UnityEngine.UI;
public class GameMgr : MonoBehaviour
{
public Text text;
#if UNITY_ANDROID
[DllImport("NDKTest")]
static extern int MyAdd(int a, int b);
#endif
void Start()
{
#if UNITY_ANDROID
text.text = MyAdd(233, 2).ToString();
#endif
}
}
因为我们做了加法之后还要乘以10,所以预期结果应该是 2350。
打包apk,安装,运行结果如下:
其他返回值
除了int的对应之外,还可以返回其他类型的变量:
字符串
//******** C **********
char* ReturnString()
{
char* a = (char*)malloc(100 * sizeof(char));
strncpy(a, "Hello World From ReturnString", 100);
return a;
}
//******** C# *********
string ReturnString();
整型数组
//******** C **********
void *ReturnIntPtr(int size)
{
int *a = (int*)malloc(size * sizeof(int));
for (int i = 0; i < size; i++)
{
a[i] = i;
}
return (void*)a;
}
//******** C# *********
IntPtr ReturnIntPtr(int size);
void Start() // C# 读取数组
{
int size = 4;
IntPtr p = ReturnIntPtr(size);
for(int i=0;i<size;i++)
{
int r = Marshal.ReadInt32(ret, i*sizeof(int));
Debug.Log("r = " + r);
}
}