unity c++扩展开发坑点总结
说明
总结下最近在开发4个平台(win64, mac, ios, android)的unity复杂C++扩展遇到的坑点,不对unity如何开发c++插件进行介绍。整个插件开发用了3-4天,各个平台的编译兼容花了1周多时间。可见有多坑。
介绍
插件架构
c# -> a.dll -> b.dll -> c.lib, d.lib
- c.lib/d.lib:是单独的一个第三方库,在win64下编译成.lib,在其他平台编译成.a
- b.dll:依赖于c.lib/d.lib,在win64下编译成.dll,在mac下编译成.bundle,ios下编译成.a,在android下编译成.so
- a.dll:依赖于b.dll,在win64下编译成.dll,在mac下编译成.bundle,ios下编译成.a,在android下编译成.so,并提供导出函数给c#使用
- c#:通过unity调用c++扩展的方式调用a.dll
总结
windows
- b.dll,c.lib,d.lib是我自己搭建的环境,用vs编译,a.dll使用的是mingw环境编译。一开始我以为两种编译的dll会不能兼容,结果发现mingw编译a.dll的时候可以直接链接vs生成的b.dll(进一步测试发现,b.dll只能在导出c函数的时候兼容,导出类的话就会出现链接失败问题)。
- b.dll编译输出后不能被重命名,即使修改a.dll的编译脚本,去链接重命名后的名字也不行。会导致将a.dll,b.dll丢入到unity中运行的时候,报**”DllNotFoundException”**错误。
- unity编辑器在使用播放按钮启动游戏之后,除非你重新打开unity,否则已经加载的dll不会被卸载。所以如果你的dll里面有静态变量依赖某些指针,而这些指针会随着游戏通过Unity播放按钮停止而删除的话,再第二次通过播放按钮打开游戏可能会导致unity崩溃。
mac
- libc.a, libd.a使用makefile编译,b.bundle与a.bundle使用的是xcode编译
- xcode编译bundle的时候是会编译32位与64位版本,所以依赖库(libc.a, libd.a)也需要编译这两个版本,使用gcc -m32/-m64分别编译对应的版本。否则在使用xcode编译的时候会出现i686, x86_64链接问题
- 小技巧1:可以使用lipo查看当前的.a支持什么版本: 可以看到libTestOtherLib.a只编译了64位版本
1
2
3Lees-iMac:TestOtherLib Netease$ lipo -info libTestOtherLib.a
input file libTestOtherLib.a is not a fat file
Non-fat file: libTestOtherLib.a is architecture: x86_64 - 小技巧2:可以使用lipo把32版本的.a和64位.a合并成一个: 可以看到libnetwork_c.a现在支持32位与64位版本
1
2
3Lees-iMac:TestOtherLib Netease$ lipo -create libnetwork_c32.a libnetwork_c64.a -output libnetwork_c.a
Lees-iMac:mac Netease$ lipo -info libnetwork_c.a
Architectures in the fat file: libnetwork_c.a are: i386 x86_64
ios
- ios不支持动态链接库,所以所有库都得编译成.a
- ios真机需要编译arm指令集(arm64, armv7, armv7s)的库,因为不清楚如何使用makefile编译这个指令集,所以都采用xcode来编译。xcode device如果选择了模拟器,则编译出来的会是x86指令集版本。这里需要将device选择位Generic iOS Device,则编译出来的会是arm指令集的库。
- arm指令集有多种,如果编译出来的arm指令集库没有包含需要的版本,则最后在链接的时候,也会错误。所以在编译的时候我们要指定需要支持的指令集,在xcode中Build Settings->Architenctures用来选择编译出来的库支持哪些指令集:
- Architectures:指定工程被编译成可支持哪些指令集类型,支持的指令集越多,对应生成二进制包就越大。
- Valid Architectures:限制可能被支持的指令集的范围,也就是Xcode编译出来的二进制包类型最终从这些类型产生,而编译出哪种指令集的包,将由Architectures与Valid Architectures的交集来确定
- Build Active Architecture Only:指定是否只对当前激活的设备所支持的指令集编译,debug一般设置为yes,它只编译当前的architecture版本,加快编译速度。release设置为no,按照Architectures和Valid Architectures的配置编译对应的所有指令集。
- 如果不确定当前编译出来的库支持什么指令集,可以使用lipo查看
- 在使用ios交叉编译luajit的时候包xxx函数找不到。这问题坑了好久,最后发现换行符的原因,将所有luajit文件CRLF改为LF之后编译通过,附上交叉编译ios版本luajit的编译命令: 因为我们编译的ios app只需要arm64版本,所以这里只编译了arm64指令集版本
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#!/usr/bin/env bash
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
SRCDIR=$DIR/luajit-2.1/
DESTDIR=$DIR/iOS
IXCODE=`xcode-select -print-path`
ISDK=$IXCODE/Platforms/iPhoneOS.platform/Developer
ISDKD=/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/
ISDKVER=iPhoneOS.sdk
ISDKP=$IXCODE/usr/bin/
if [ ! -e $ISDKP/ar ]; then
sudo cp $ISDKD/usr/bin/ar $ISDKP
fi
if [ ! -e $ISDKP/ranlib ]; then
sudo cp $ISDKD/usr/bin/ranlib $ISDKP
fi
cd $SRCDIR
make clean
ISDKF="-arch arm64 -isysroot $ISDK/SDKs/$ISDKVER -miphoneos-version-min=8.0 -fembed-bitcode"
make HOST_CC="gcc " TARGET_FLAGS="$ISDKF" TARGET=arm64 TARGET_SYS=iOS BUILDMODE=static
mv -f "$SRCDIR"/src/libluajit.a "$DESTDIR"/libluajit.a
make clean
android
- android jni有两个配置文件,Application.mk和Android.mk。Application.mk配置全局相关内容,Android.mk配置编译相关命令。一开始我只知道Android.mk,全局变量APP_ABI是通过命令行传入配置的,这在编译纯C库的时候没遇到什么问题。但是在编译c++库的时候就遇到大坑了,无论我在Android.mk里面如何设置APP_STL,都感觉起不到这个变量的作用,还是会报找不到头文件。最后看到Application.mk文件时候才知道需要设置在Appliction.mk里面。原谅我第一次搞mk脚本。所以总结类似于APP_ABI, APP_STL这种全局配置变量需要设置在Application.mk中
- 编译libb.so或者liba.so的时候,因为依赖其他库,Android.mk中不能简单的直接在LOCAL_STATIC_LIBRARIES := 中加入依赖的库,需要在之前将依赖库加入到PREBUILT_STATIC_LIBRARY中。例如这里的network_c依赖libenet.a,libkcp.a的Android.mk脚本:
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
30
31
32
33LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := enet
LOCAL_SRC_FILES := ../../enet/android/lib/$(TARGET_ARCH_ABI)/libenet.a # 因为APP_ABI里指定了两个版本(x86,armeabi-v7a),所以可以使用这个宏来表示现在正在编译哪个版本
include $(PREBUILT_STATIC_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := kcp
LOCAL_SRC_FILES := ../../kcp/android/lib/$(TARGET_ARCH_ABI)/libkcp.a
include $(PREBUILT_STATIC_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := z
LOCAL_SRC_FILES := ../../zlib/android/lib/$(TARGET_ARCH_ABI)/libz.a
include $(PREBUILT_STATIC_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := network_c
LOCAL_C_INCLUDES := $(LOCAL_PATH)/../ \
$(LOCAL_PATH)/../../enet/android/include \
$(LOCAL_PATH)/../../kcp/android/include \
$(LOCAL_PATH)/../../zlib/android/include
LOCAL_STATIC_LIBRARIES := enet kcp z
LOCAL_CPPFLAGS := -std=c++11
MY_CPP_LIST := $(wildcard $(LOCAL_PATH)/../*.cpp)
LOCAL_SRC_FILES := $(MY_CPP_LIST:$(LOCAL_PATH)/%=%)
include $(BUILD_SHARED_LIBRARY) - windows下如果在批处理脚本里调用ndk-build命令的话,需要在之前加入call,例如
call ndk-build
,因为windows下ndk-build自己本身就是一个批处理
写在最后
我第一次接触mac、ios、android的平台编译,如果有错误,请大家指出,共同进步。