Cross-platform C/C++ Plugins in Unity
Have you ever wanted to use a native library in Unity or write parts of your game in super portable, efficient C/C++?
Recently I stumbled over an interesting C++ library and integrated it in a Unity project. Let me show you what I did.
I'll focus more on the workflow and tools to create comprehensive multi-platform support than on the actual interface source code. There's already quite good documentation about that[1].
The target
The library in focus is Basis Universal (see this other post for more info). It consists of:
- an encoder that converts images into the basis file format
- a transcoder that converts the basis file into GPU-friendly format at runtime
The transcoder is what I was interested in. It's also a great example, since it has a straightforward API, consist of a single file (header files not counting) and has no 3rd party dependencies.
Let's get started
In this post I'll only give code excerpts and examples. To see the complete code look at BasisUniversalUnityBuild (for building the native libraries) and BasisUniversalUnity (the matching C# interface).
Inspect the library
First, let's look how it's supposed to be used[2]. In a nutshell like this (warning: pseudo code):
#include "transcoder/basisu_transcoder.h"
// one-time initialization at startup
basist::basisu_transcoder_init();
basist::etc1_global_selector_codebook sel_codebook(basist::g_global_selector_cb_size, basist::g_global_selector_cb);
// load the basis file content into a buffer
uint8_t * data = ...
size_t length = ...
// creating a basis_file instance
basis_file* new_basis = new basis_file(data,length);
// and now you can do things like retrieving the image size
uint32_t image_index = 0; // first image
uint32_t level_index = 0; // highest mipmap level
uint32_t width = new_basis->getImageWidth(image_index,level_index);
// and finally transcoding
new_basis->transcodeImage(destination_buffer, dst_size, image_index ,level_index , ...);
Wrapping
You can call native C functions from C# by using the DllImport
method attribute[1:1]:
[DllImport ("PluginName")]
private static extern void some_function ();
No, I did not forget the ++
. You can only call pure C functions and use pure C data types (no C++ classes) from C#. Luckily C and C++ are compatible, so we need to wrap the logic within C functions like this:
// the extern "C" block tells the compiler to use C name mangling instead of C++
extern "C" {
void aa_basis_init()
{
basisu_transcoder_init();
}
}
Build via CMake
After writing all C wrapper code we need to build the libraries. In the end, I want this to work on multiple platforms:
- WebGL
- macOS 64-bit
- Windows
- 32-bit
- 64-bit
- iOS
- armv7
- armv7s
- arm64
- Android
- armeabi-v7a
- arm64-v8a
- Linux 64-bit
The library needs to be built for each one of these, so it makes sense to use a build system. Since I'm familiar with it, I chose CMake.
CMake rules
All you have to do is describe to CMake what you want to build/compile and it takes care of creating projects and invoking the right compiler/linker commands. This description has a certain syntax and needs to be saved to a CMakeLists.txt
file. This is how a minimal version looks like:
# define a CMake version
cmake_minimum_required(VERSION 3.0)
# give the project a name
project(basisu_transcoder)
# create a list of files to compile
set(BASISU_SRC_LIST
# this is the transcoder itself
basis_universal/transcoder/basisu_transcoder.cpp
# this is my wrapper code
basisu_wrapper/basisu_wrapper.cpp
)
# add a library target
add_library(basisu SHARED ${BASISU_SRC_LIST})
# In order to find header files, we have to tell the compiler in which
# folders he's got to search: include directories
target_include_directories(
basisu
PUBLIC
basis_universal/transcoder
)
In theory you can now invoke CMake and build the library. Unfortunately in order to get all platform libraries in a way they work in Unity I had fix some more things:
- macOS Unity requires a
.bundle
file library without thelib
prefix. - iOS
- lib has to be static
- A special toolchain is needed
- Android
- special toolchain
- Windows
- Had to setup everything on another machine
- Enable
__declspec
via compile definition (pre-processor) when using MSVC compiler
- Installing
- Install lib to correct destination folders in Unity project
- Copy the sources for WebGL builds
I won't go into all details, but I want to point out the Installing part.
Install to correct destination
Within the consuming Unity project's Assets folder, we need our libraries at certain destinations. Here's an (incomplete) overview how it should look like:
Plugins
├── Android
│ └── libs
│ └── arm64-v8a
│ └── libbasisu.so
├── WebGL
│ ├── basisu_wrapper.cpp
│ ├── basisu_transcoder.cpp
│ └── ...some more header files
├── iOS
│ └── libbasisu.a
├── x86
│ └── basisu.dll
└── x86_64
├── basisu.dll
├── basisu.bundle
└── basisu.so
In order not to have to manually copy every library file into the Unity project, I tweaked the CMake install target.
...
# first, add a path parameter/option where the user can provide the path
set( BASIS_UNIVERSAL_UNITY_PATH "" CACHE PATH "Path locating the BasisUniversalUnity package source. When installing, native libraries will get injected there" )
# Set the output sub-path within the package project
# In this case for standalone x64 targets
set( DEST_PLUGIN_PATH "${BASIS_UNIVERSAL_UNITY_PATH}/Runtime/Plugins/x86_64")
# We can check if it's correct by logging it
message(STATUS "Will install native libs to ${DEST_PLUGIN_PATH}")
# Check if the path actually exists
if( NOT EXISTS ${DEST_PLUGIN_PATH})
message(SEND_ERROR "Invalid path!")
endif()
# Finally tell CMake to install to our path
install(TARGETS basisu DESTINATION ${DEST_PLUGIN_PATH})
This was a huge time saver!
WebGL
WebGL is a special case when it comes to native libraries. You cannot build/provide a pre-compiled library for WebGL, but you place all C++ source files within the Assets/Plugins/WebGL
folder. Unity will detect and include them when compiling the rest of the game code (which is also C++ generated from C# via IL2CPP).
All we need to do is copy the right source files over to this destination, which I also did via CMake:
# Set the right sub-folder
set( UNITY_PLUGIN_DIR_WEBGL "${BASIS_UNIVERSAL_UNITY_PATH}/Runtime/Plugins/WebGL")
# Let the install target copy the listed source files
install(
FILES
# The two source files
basisu_wrapper/basisu_wrapper.cpp
basis_universal/transcoder/basisu_transcoder.cpp
# Plus all the header files they reference
basis_universal/transcoder/basisu_transcoder.h
basis_universal/transcoder/basisu_transcoder_internal.h
basis_universal/transcoder/basisu_global_selector_cb.h
basis_universal/transcoder/basisu_transcoder_tables_bc7_m6.inc
basis_universal/transcoder/basisu_global_selector_palette.h
basis_universal/transcoder/basisu.h
basis_universal/transcoder/basisu_transcoder_tables_dxt1_6.inc
basis_universal/transcoder/basisu_file_headers.h
basis_universal/transcoder/basisu_transcoder_tables_dxt1_5.inc
DESTINATION
${UNITY_PLUGIN_DIR_WEBGL}
)
The only down-side is that this is always done as part of another platform's build (in this case redundantly every other platform), but that's not really a problem.
Configure build and generate
Now we have a final CMakeLists.txt and we are ready to generate all Xcode / Visual Studio projects or Makefiles and fire up the compilers.
The extensive documentation how this is done can be found at the BasisUniversalUnityBuild project site.
Future work
Linux
I haven't built the Linux libraries yet. Definitely will, especially since the Unity Linux Editor is coming out of preview later this year.
Continuous Integration
Although I made some small improvements, building a library for so many platforms requires a lot of small steps. It would be nice to further automate the building process, so updating the library is fast and less error prone.
This means either having multiple build workers with different platforms or doing more cross-compiling. I already cross-compiled for iOS and Android from my machine (macOS).Adding Windows and Linux as target platform would be nice for development. On the other hand I'd love to be able to cross-compile all variants from one server platform (preferably Linux). Maybe I'll look into that some day.
Unity Package
To be able to re-use the library (all native libs plus the C# interface code) amongst multiple Unity projects easily, the proper way is to create a custom Unity package. I did this in this particular case ( see BasisUniversalUnity on GitHub ) and plan to write about it in the future.
If you liked this read, feel free to