This guide covers cross-compiling and running subspace on an Android device or emulator from a macOS (Apple Silicon) host.
| Tool | Install |
|---|---|
| OpenJDK 17 | brew install openjdk@17 |
| Android CLI tools | brew install --cask android-commandlinetools |
| NDK 27 | via sdkmanager (see below) |
| ARM64 system image | via sdkmanager |
Install SDK components (accepts licenses automatically):
export JAVA_HOME="/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home"
export ANDROID_HOME="/opt/homebrew/share/android-commandlinetools"
yes | sdkmanager --sdk_root="$ANDROID_HOME" \
"platform-tools" \
"platforms;android-34" \
"ndk;27.0.12077973" \
"system-images;android-34;google_apis;arm64-v8a" \
"emulator"export ANDROID_HOME="/opt/homebrew/share/android-commandlinetools"
export ANDROID_NDK_HOME="$ANDROID_HOME/ndk/27.0.12077973"
export PATH="/opt/homebrew/opt/openjdk@17/bin:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator:$PATH"Create and boot an ARM64 AVD:
avdmanager create avd -n subspace_test \
-k "system-images;android-34;google_apis;arm64-v8a" \
--device "pixel_6"
emulator -avd subspace_test -no-window -no-audio -gpu swiftshader_indirect &
adb wait-for-deviceBazel is the simplest way to cross-compile Subspace Android artifacts from this
repository because it fetches its own C++ dependencies and uses the NDK
toolchain configured in .bazelrc.
Build the server, native tests, JNI library, and Java client test for Android ARM64:
bazelisk build \
//server:subspace_server \
//client:client_test \
//c_client:client_test \
//plugins:nop_plugin.so \
//plugins:split_buffer_free_test_plugin.so \
//android/jni:libsubspace_jni.so \
//android/java:subspace-java \
//android/java:subspace-java-test \
--config=android_arm64The android_arm64 config in .bazelrc sets:
--platforms=//platform/android:android_arm64--cpu=aarch64(prevents legacy macOS config_settings from matching)--linkopt=-lc++_static --linkopt=-lc++abi(NDK C++ stdlib)--action_env=ANDROID_NDK_HOME
For an x86_64 emulator or CI runner, use --config=android_x86_64 instead.
The emulator with google_apis images supports adb root:
adb rootThe default server socket on Android is /data/local/tmp/subspace (defined by
kDefaultServerSocket in client/client.h). This path is writable without
root.
Bazel produces shared libraries as symlinks in bazel-bin/_solib_arm64-v8a/.
You must dereference them before pushing to the device:
# Dereference shared library symlinks
rm -rf /tmp/android_libs
mkdir -p /tmp/android_libs
cp -L bazel-bin/_solib_arm64-v8a/*.so /tmp/android_libs/
# Push libraries and binaries
adb push /tmp/android_libs/ /data/local/tmp/android_libs/
adb push bazel-bin/server/subspace_server /data/local/tmp/
adb push bazel-bin/client/client_test /data/local/tmp/
adb shell "chmod 755 /data/local/tmp/subspace_server /data/local/tmp/client_test"
# Push plugins
adb shell "mkdir -p /data/local/tmp/plugins"
adb push bazel-bin/plugins/nop_plugin.so /data/local/tmp/plugins/
adb push bazel-bin/plugins/split_buffer_free_test_plugin.so /data/local/tmp/plugins/adb shell "cd /data/local/tmp && ./subspace_server &"The server uses the default socket /data/local/tmp/subspace on Android. Shared
memory is fd-backed and does not require a device-visible directory.
adb shell "cd /data/local/tmp && LD_LIBRARY_PATH=/data/local/tmp/android_libs ./client_test"LD_LIBRARY_PATH is required because the test binary links against shared
libraries that live in the android_libs/ directory.
To run a specific test:
adb shell "cd /data/local/tmp && LD_LIBRARY_PATH=/data/local/tmp/android_libs \
./client_test --gtest_filter='ClientTest.Init'"Android lacks POSIX shared memory (shm_open/shm_unlink). Subspace uses
SUBSPACE_SHMEM_MODE_MEMFD (defined in common/channel.h) which:
- Creates anonymous
memfd_create()regions for the SCB, CCB, BCB, and client-owned message buffers. - Sizes them with
ftruncate()and maps them withmmap(). - Passes file descriptors between processes via Unix domain sockets
(
SCM_RIGHTS). - Keeps client-side buffer allocation and resize by registering publisher-owned buffer FDs with the server, which brokers them to subscribers.
SUBSPACE_SHMEM_MODE_MEMFD names the mechanism, not the platform: Android always
uses it, but it can also be selected on Linux (independently of Android) by
building with --config=linux_memfd (Bazel) or -DSUBSPACE_LINUX_USE_MEMFD=ON
(CMake). The default Linux backend remains named /dev/shm objects
(SUBSPACE_SHMEM_MODE_LINUX); macOS/QNX (SUBSPACE_SHMEM_MODE_POSIX) are
unaffected. Note that memfd mode routes buffer creation/resize through the
server (anonymous FDs must be passed via SCM_RIGHTS), whereas the named
/dev/shm backend maps buffers directly by name without server round-trips.
Built-in split buffers also use anonymous FDs. The publisher registers the prefix and slot FDs with the server; subscribers fetch those descriptors when attaching to a new buffer generation. Custom split-buffer callbacks continue to use the callback-provided handles.
Android enforces linker namespace restrictions. Shared libraries must be in a
directory referenced by LD_LIBRARY_PATH or in the same directory as the
executable. The android_libs/ approach works for /data/local/tmp/ binaries.
Subspace can also be cross-compiled for Android using CMake with the NDK
toolchain. CMake fetches the same third-party dependencies with
FetchContent, but protobuf code generation requires a host-native protoc
that matches the protobuf version used by the Android build.
export ANDROID_NDK_HOME=/path/to/ndk
# Build a host protoc matching Subspace's protobuf dependency.
cmake -S . -B build/host-protoc -DCMAKE_BUILD_TYPE=Release
cmake --build build/host-protoc --target protoc --parallel
cmake -S . -B build/android \
-DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake \
-DANDROID_ABI=arm64-v8a \
-DANDROID_PLATFORM=android-28 \
-DANDROID_STL=c++_shared \
-DCMAKE_BUILD_TYPE=Release \
-DPROTOC_EXECUTABLE="$PWD/build/host-protoc/_deps/protobuf-build/protoc"
cmake --build build/android --parallelUse -DANDROID_ABI=x86_64 for an x86_64 emulator. The Android CMake build
produces the native test binaries plus the Java/JNI artifacts:
build/android/server/subspace_serverbuild/android/client/client_testbuild/android/c_client/c_client_testbuild/android/android/libsubspace_jni.sobuild/android/android/subspace-java.jarbuild/android/android/subspace-java-test.jar
The CI helper script .github/scripts/cmake-android-test.sh shows the expected
deployment layout and how to run the CMake-built tests on an emulator.
Subspace provides Android.bp files for building as part of an AOSP source
tree using the Soong build system. This is the recommended approach for
integrating subspace into an Android platform image.
Soong builds require a full AOSP checkout. GitHub-hosted runners do not provide one by default; use a local AOSP tree or a self-hosted CI runner with AOSP already synced.
Place the subspace source tree in your AOSP checkout (e.g.,
external/subspace/). The Blueprint files define these modules:
| Module | Type | Description |
|---|---|---|
libsubspace_common |
static lib | Core channel, shared memory, syscall shim |
libsubspace_client |
shared lib | Client API (publisher/subscriber) |
libsubspace_server |
static lib | Server implementation |
subspace_server |
binary | Standalone server daemon |
libsubspace_proto |
static lib | Protobuf message definitions |
libsubspace_jni |
shared lib | JNI bindings for Java clients |
subspace-java |
java lib | Java client wrapper |
subspace_java_client_test |
java binary | Device-side Java integration test |
subspace_rpc |
host binary | RPC IDL compiler (protoc plugin) |
librpc_server |
static lib | co::Coroutine RPC server runtime |
librpc_client |
static lib | co::Coroutine RPC client runtime |
librpc_test_proto |
static lib | Test-service protobuf messages |
librpc_test_subspace_rpc |
static lib | Generated test-service client/server stubs |
The RPC modules live in rpc/Android.bp. To build an RPC service for Android,
link librpc_client / librpc_server and generate stubs from your .proto
files with the subspace_rpc plugin via a genrule (see
librpc_test_subspace_rpc_gen in rpc/Android.bp for the protoc invocation).
Subspace requires two external libraries that must also be present in the AOSP tree:
- coroutines (
external/coroutines/) — https://github.com/dallison/coroutines - cpp_toolbelt (
external/cpp_toolbelt/) — https://github.com/dallison/cpp_toolbelt
Example Blueprint files for both are provided in
external/coroutines/Android.bp.example and
external/cpp_toolbelt/Android.bp.example within this repository. Copy these to
Android.bp in the respective source trees in your AOSP checkout.
The following modules must be available in the AOSP tree (they are part of standard AOSP):
libprotobuf-cpp-full— Protocol Buffers runtime.subspace.protousesgoogle.protobuf.Any, which is not available from the lite runtime.liblog— Android logginglibdl— Dynamic linkerlibabsl— Abseil runtime and headersjni_headers— JNI headers (for the JNI module)
Add Subspace to a product makefile:
PRODUCT_SOONG_NAMESPACES += external/subspace
PRODUCT_PACKAGES += \
subspace_server \
libsubspace_client \
libsubspace_jni \
subspace-java \
subspace_java_client_testThen build from your AOSP root:
source build/envsetup.sh
lunch <product>-userdebug
m external.subspace-subspace_server-soong \
external.subspace-libsubspace_client-soong \
external.subspace-libsubspace_jni-soong \
external.subspace-subspace-java-soong \
external.subspace-subspace_java_client_test-soongAfter flashing or installing those artifacts on a device image, the Java integration test can be run through its wrapper:
adb shell subspace_java_client_test- The
subspace_defaultsmodule in the rootAndroid.bpsets C++17 mode, warning flags, exceptions/RTTI, and the-DSUBSPACE_ANDROIDpreprocessor define. - All modules use
stl: "c++_shared"andmin_sdk_version: "28". - The server binary can be included in the system partition via
PRODUCT_PACKAGES += subspace_serverin your device makefile. - The JNI library and Java wrapper can be included in apps via the standard AOSP module dependency mechanism.
A Subspace server inside the emulator can bridge channels to a server running natively on the host, in both directions (publish on Android / subscribe on the host, and vice versa). The emulator's user‑mode network is a NAT where only the guest can initiate connections and UDP broadcast does not cross the boundary, so two server features are used:
--tcp_discovery— run discovery over a single TCP connection that the guest dials to the host (instead of UDP broadcast/unicast). Seeserver-architecture.mdfor details.--bridge_advertise_address=127.0.0.1— advertise a loopback endpoint for bridge listeners, reached throughadbport tunnels.
The host and guest use different bridge ports so the adb forward/reverse
loopback listeners never collide with a server's own listener. With the host
acting as the discovery listener:
# Tunnels: guest dials out to the host (reverse); host dials into the guest (forward).
adb reverse tcp:6502 tcp:6502 # discovery: guest -> host
adb reverse tcp:7100 tcp:7100 # bridge data: guest publisher -> host subscriber
adb forward tcp:7200 tcp:7200 # bridge data: host publisher -> guest subscriber
# Host (discovery listener):
./subspace_server --socket=/tmp/subspace_host --tcp_discovery --disc_port=6502 \
--bridge_ports=7100 --bridge_advertise_address=127.0.0.1 \
--cleanup_filesystem=false
# Guest (discovery connector), inside the emulator:
adb shell "/data/local/tmp/subspace_server --socket=/data/local/tmp/subspace \
--tcp_discovery --peer_address=127.0.0.1 --peer_port=6502 \
--bridge_ports=7200 --bridge_advertise_address=127.0.0.1"Publishers must be created with local=false for their channels to bridge.
The script manual_tests/cross_host_bridge.sh automates this end to end and
verifies both directions:
manual_tests/cross_host_bridge.sh native # two servers on the host (loopback)
manual_tests/cross_host_bridge.sh emulator # host <-> running emulator