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

  1. b.dll,c.lib,d.lib是我自己搭建的环境,用vs编译,a.dll使用的是mingw环境编译。一开始我以为两种编译的dll会不能兼容,结果发现mingw编译a.dll的时候可以直接链接vs生成的b.dll(进一步测试发现,b.dll只能在导出c函数的时候兼容,导出类的话就会出现链接失败问题)。
  2. b.dll编译输出后不能被重命名,即使修改a.dll的编译脚本,去链接重命名后的名字也不行。会导致将a.dll,b.dll丢入到unity中运行的时候,报**”DllNotFoundException”**错误。
  3. unity编辑器在使用播放按钮启动游戏之后,除非你重新打开unity,否则已经加载的dll不会被卸载。所以如果你的dll里面有静态变量依赖某些指针,而这些指针会随着游戏通过Unity播放按钮停止而删除的话,再第二次通过播放按钮打开游戏可能会导致unity崩溃。

mac

  1. libc.a, libd.a使用makefile编译,b.bundle与a.bundle使用的是xcode编译
  2. xcode编译bundle的时候是会编译32位与64位版本,所以依赖库(libc.a, libd.a)也需要编译这两个版本,使用gcc -m32/-m64分别编译对应的版本。否则在使用xcode编译的时候会出现i686, x86_64链接问题
  3. 小技巧1:可以使用lipo查看当前的.a支持什么版本:
    1
    2
    3
    Lees-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
    可以看到libTestOtherLib.a只编译了64位版本
  4. 小技巧2:可以使用lipo把32版本的.a和64位.a合并成一个:
    1
    2
    3
    Lees-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
    可以看到libnetwork_c.a现在支持32位与64位版本

ios

  1. ios不支持动态链接库,所以所有库都得编译成.a
  2. ios真机需要编译arm指令集(arm64, armv7, armv7s)的库,因为不清楚如何使用makefile编译这个指令集,所以都采用xcode来编译。xcode device如果选择了模拟器,则编译出来的会是x86指令集版本。这里需要将device选择位Generic iOS Device,则编译出来的会是arm指令集的库。
  3. 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的配置编译对应的所有指令集。
  4. 如果不确定当前编译出来的库支持什么指令集,可以使用lipo查看
  5. 在使用ios交叉编译luajit的时候包xxx函数找不到。这问题坑了好久,最后发现换行符的原因,将所有luajit文件CRLF改为LF之后编译通过,附上交叉编译ios版本luajit的编译命令:
    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
    因为我们编译的ios app只需要arm64版本,所以这里只编译了arm64指令集版本

android

  1. 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中
  2. 编译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
    33
    LOCAL_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)
  3. windows下如果在批处理脚本里调用ndk-build命令的话,需要在之前加入call,例如call ndk-build,因为windows下ndk-build自己本身就是一个批处理

写在最后

我第一次接触mac、ios、android的平台编译,如果有错误,请大家指出,共同进步。