Robolectric Native Runtime: Architecture & Implementation Report
1. Introduction
Robolectric is the de-facto standard for running Android unit tests on the JVM without a device or emulator. Historically, Robolectric used hand-written Java “shadow” classes to approximate the behavior of Android framework native code. Starting around Android O (API 26), Robolectric introduced Native Runtime — a mechanism to load real AOSP native libraries (compiled for host platforms) and delegate Android framework native method calls to them, yielding pixel-accurate graphics, real SQLite behavior, and authentic text layout.
This report documents how the Robolectric Native Runtime works within the AOSP main source tree: its architecture, the native libraries involved, how they are compiled for host platforms, how they are integrated via JNI, and how the shadow layer delegates to them.
2. High-Level Architecture
┌──────────────────────────────────────────────────────────────────────┐
│ Robolectric Test (JVM) │
├──────────────────────────────────────────────────────────────────────┤
│ Test Code (JUnit) │
│ │ │
│ ▼ │
│ Android Framework Classes (android.graphics.*, android.database.*) │
│ │ (bytecode-instrumented by Robolectric Sandbox) │
│ ▼ │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ Shadow Layer (ShadowNative* classes) │ │
│ │ ShadowNativePaint, ShadowNativeBitmap, ShadowNativeCanvas, │ │
│ │ ShadowNativeSQLiteConnection, ShadowNativeTypeface, ... │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ Natives Delegate Classes (*Natives.java) │ │
│ │ PaintNatives, BitmapNatives, SQLiteConnectionNatives, ... │ │
│ │ │ (declare Java native methods) │ │
│ │ ▼ │ │
│ │ DefaultNativeRuntimeLoader │ │
│ │ - Detects OS/arch, extracts .so/.dylib/.dll from JAR │ │
│ │ - Sets system properties for class registration │ │
│ │ - Calls System.load() on the native library │ │
│ │ - Copies fonts, ICU data to temp directory │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │ JNI / System.load() │
│ ▼ │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ Host Native Shared Library (libandroid_runtime.so) │ │
│ │ ┌──────────────────────────────────────────────────────┐ │ │
│ │ │ HostRuntime.cpp (JNI_OnLoad entry point) │ │ │
│ │ │ → register_android_core_classes() │ │ │
│ │ │ → register_android_graphics_classes() │ │ │
│ │ │ → loadIcuData(), property_initialize_ro_cpu_abilist │ │ │
│ │ └──────────────────────────────────────────────────────┘ │ │
│ │ ┌──────────────────────────────────────────────────────┐ │ │
│ │ │ libhwui (statically linked for host) │ │ │
│ │ │ - Skia CPU pipeline (HWUI_NULL_GPU mode) │ │ │
│ │ │ - Bitmap, Canvas, Paint, Path, Typeface, etc. │ │ │
│ │ │ - Text: LineBreaker, MeasuredText, TextRunShaper │ │ │
│ │ │ - RenderNode, RenderEffect, HardwareRenderer │ │ │
│ │ └──────────────────────────────────────────────────────┘ │ │
│ │ ┌──────────────────────────────────────────────────────┐ │ │
│ │ │ Other statically linked host libraries │ │ │
│ │ │ libskia, libandroidfw, libsqlite, libminikin, │ │ │
│ │ │ libharfbuzz_ng, libft2, libpng, libjpeg, libwebp, │ │ │
│ │ │ libicu, libz, libhostgraphics, ... │ │ │
│ │ └──────────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────┘
3. Supported Host Platforms
The native runtime supports the following host configurations
(from DefaultNativeRuntimeLoader.isSupported()):
| OS | Architecture | Library Name |
|---|---|---|
| Linux | x86_64 | libandroid_runtime.so (V+) / librobolectric-nativeruntime.so (pre-V) |
| macOS | x86_64 | libandroid_runtime.dylib (V+) / librobolectric-nativeruntime.dylib (pre-V) |
| macOS | aarch64 | libandroid_runtime.dylib (V+) / librobolectric-nativeruntime.dylib (pre-V) |
| Windows | x86_64 | libandroid_runtime.dll (V+) / robolectric-nativeruntime.dll (pre-V) |
Source evidence:
DefaultNativeRuntimeLoader.javalines 325–329:return (OsUtil.isMac() && (Objects.equals(arch(), "aarch64") || Objects.equals(arch(), "x86_64"))) || (OsUtil.isLinux() && Objects.equals(arch(), "x86_64")) || (OsUtil.isWindows() && Objects.equals(arch(), "x86_64"));
4. Native Libraries: What Gets Compiled & How
4.1 The Core Library: libandroid_runtime
The primary native shared library is libandroid_runtime, defined in
frameworks/base/core/jni/Android.bp (line 512).
Key characteristics:
host_supported: true— enabled for host builds- Inherits from
libandroid_runtime_defaultswhich includes both core JNI and graphics JNI code - On host, uses a static linking strategy for most dependencies (libhwui, libskia, libsqlite,
etc.) to produce a self-contained
.so
Host-specific source files (frameworks/base/core/jni/Android.bp lines 440–508):
platform/host/HostRuntime.cpp ← Main JNI_OnLoad entry point
platform/host/native_window_jni.cpp ← Stub native window for host
Host-specific static libraries (line 451–479):
libandroidfw (Android Framework resources)
libhostgraphics (Host stubs for graphics surfaces)
libhwui (Hardware UI / graphics engine)
libft2 (FreeType font rendering)
libharfbuzz_ng (Text shaping)
libminikin (Text layout)
libsqlite (SQLite database engine)
libskia (2D graphics engine)
libpng, libjpeg, libwebp (Image codecs)
libz (Compression)
libnativehelper_jvm (JNI helper for host JVM)
libultrahdr (Ultra HDR support)
Source evidence:
frameworks/base/core/jni/Android.bplines 451–479.
4.2 libhwui — The Graphics Engine
libhwui is defined in frameworks/base/libs/hwui/Android.bp (line 726) with
host_supported: true.
Host mode differences:
- Compiled with
-DHWUI_NULL_GPU— disables GPU acceleration, uses Skia CPU pipeline only - Uses
platform/host/stub implementations for:renderthread/RenderThread.cpp— simplified single-threaded renderingrenderthread/CacheManager.cpp— no GPU cache neededReadback.cpp— stub readbackWebViewFunctorManager.cpp— no WebView support on host
- Includes
libhostgraphicsas substitute for reallibgui/libnativewindow
Source evidence:
frameworks/base/libs/hwui/Android.bplines 699–722:host: { srcs: [ "platform/host/renderthread/CacheManager.cpp", "platform/host/renderthread/RenderThread.cpp", ... ], cflags: [ "-DHWUI_NULL_GPU", "-DNULL_GPU_MAX_TEXTURE_SIZE=4096", ], },
4.3 libhostgraphics — Host Surface Stubs
Defined in frameworks/base/libs/hostgraphics/Android.bp, this library provides
host-compatible stubs for Android-specific display and surface APIs:
ANativeWindow.cpp— stub native windowFence.cpp— stub sync fenceHostBufferQueue.cpp— stub buffer queuePublicFormat.cpp— image format utilitiesADisplay.cpp— stub display
4.4 Pre-V: librobolectric-nativeruntime (Prebuilt)
For Android SDK versions prior to V (VanillaIceCream), the native library is called
librobolectric-nativeruntime and ships as prebuilt binaries in two forms:
- In AOSP:
prebuilts/misc/common/robolectric-native-prebuilt/native/linux/x86_64/librobolectric-nativeruntime.sonative/mac/librobolectric-nativeruntime.dylib
- In Gradle/Maven: The
nativeruntime-dist-compatartifact (version 1.0.17) published to Maven Central.
Source evidence:
prebuilts/misc/common/robolectric-native-prebuilt/Android.bp:// Releases including and since VanillaIceCream ships with a libandroid_runtime.so // equivalent to the librobolectric-nativeruntime.so included in this artifact. name: "robolectric_nativeruntime_native_prebuilt"
4.5 V+ (Android 15+): libandroid_runtime (Built from AOSP)
Starting with Android V, Robolectric uses the same libandroid_runtime.so that AOSP builds
for host. This library is packaged as a JAR via a java_genrule:
Source evidence:
external/robolectric/Android.bplines 65–75:java_genrule { name: "libandroid_runtime_jar", host_first_srcs: [":libandroid_runtime"], out: ["libandroid_runtime.jar"], cmd: "mkdir -p ./native/linux/x86_64/ && " + "cp $(location :libandroid_runtime) ./native/linux/x86_64/ && " + "$(location soong_zip) -o $(location libandroid_runtime.jar) -D ./native", }
The resulting JAR is then bundled into robolectric-host-android_all (line 123–157).
5. JNI Registration Mechanism
5.1 Two-Phase Registration (V+)
For Android V and above, JNI registration uses a class-name-driven dynamic lookup mechanism. The classes to register are communicated via Java system properties:
System.setProperty("core_native_classes", "android.database.CursorWindow,...");
System.setProperty("graphics_native_classes", "android.graphics.Bitmap,...");
System.setProperty("method_binding_format", "$$robo$$${method}$nativeBinding");
Phase 1 — Core Classes (from HostRuntime.cpp → register_android_core_classes()):
- Reads
core_native_classessystem property - Looks up each class name in
gRegJNIMap(anunordered_map<string, RegJNIRec>) - Calls the corresponding
register_*()function
Phase 2 — Graphics Classes (from LayoutlibLoader.cpp → register_android_graphics_classes()):
- Reads
graphics_native_classessystem property - Uses a similar map in
LayoutlibLoader.cpp
5.2 Method Name Rewriting
A critical mechanism is JNI method name rewriting. Normally, JNI maps native void nFoo() in
class android.graphics.Paint to a C function Java_android_graphics_Paint_nFoo. But Robolectric’s
bytecode instrumentor replaces native methods with Java stubs and creates a separate native
binding method with a mangled name.
The format is: $$robo$$<method>$nativeBinding
This is set via:
System.setProperty("method_binding_format", "$$robo$$${method}$nativeBinding");
On the native side, graphics_jni_helpers.h contains jniRegisterMaybeRenamedNativeMethods()
which transforms each JNI method registration to use the rewritten name format:
// For method "nInit", registers as "$$robo$$nInit$nativeBinding" instead
std::string modifiedName = jniMethodFormat;
modifiedName.replace(methodNamePos, 9, gMethods[i].name);
Source evidence:
frameworks/base/libs/hwui/jni/graphics_jni_helpers.hlines 95–123.
5.3 Bytecode Instrumentation
Robolectric’s ClassInstrumentor (in the sandbox module) transforms native methods:
- Original:
native long nInit()inandroid.graphics.Paint - After instrumentation:
nInit()— becomes a non-native Java method that delegates to shadow/handler$$robo$$nInit$nativeBinding()— a new native method for actual JNI binding
When callNativeMethodsByDefault = true on a shadow and no explicit shadow method is found,
ShadowWrangler routes the call to the $nativeBinding method:
// ShadowWrangler.java line 166-168
Method method = definingClass.getDeclaredMethod(
ShadowConstants.ROBO_PREFIX + name + "$nativeBinding", paramTypes);
Source evidence:
sandbox/src/main/java/.../ClassInstrumentor.javalines 574–589,ShadowWrangler.javalines 160–174.
6. Core Native Class Registrations
6.1 Core Classes (CORE_CLASS_NATIVES)
These are non-graphics native classes registered for Android V+:
| Class | Native Registration Function | Primary Native Library |
|---|---|---|
android.animation.PropertyValuesHolder |
register_android_animation_PropertyValuesHolder |
libandroid_runtime |
android.database.CursorWindow |
register_android_database_CursorWindow |
libandroid_runtime |
android.database.sqlite.SQLiteConnection |
register_android_database_SQLiteConnection |
libandroid_runtime → libsqlite |
android.database.sqlite.SQLiteRawStatement |
register_android_database_SQLiteRawStatement |
libandroid_runtime → libsqlite |
android.media.ImageReader |
register_android_media_ImageReader |
libandroid_runtime |
android.view.Surface |
register_android_view_Surface |
libandroid_runtime |
com.android.internal.util.VirtualRefBasePtr |
register_com_android_internal_util_VirtualRefBasePtr |
libandroid_runtime |
libcore.util.NativeAllocationRegistry |
register_libcore_util_NativeAllocationRegistry |
HostRuntime.cpp |
Source evidence:
DefaultNativeRuntimeLoader.javalines 59–70.
6.2 Graphics Classes (GRAPHICS_CLASS_NATIVES)
These are graphics-related native classes (40+ classes):
| Category | Classes |
|---|---|
| Bitmap | Bitmap, BitmapFactory, NinePatch, ImageDecoder |
| Canvas | Canvas, RecordingCanvas, Picture |
| Drawing | Paint, Shader, ColorFilter, MaskFilter, PathEffect, RenderEffect |
| Geometry | Path, PathIterator, PathMeasure, Matrix, Region |
| Text | Typeface, Font, FontFamily, LineBreaker, MeasuredText, TextRunShaper |
| Render | RenderNode, HardwareRenderer, HardwareRendererObserver |
| Animation | AnimatedVectorDrawable, AnimatedImageDrawable, VectorDrawable, NativeInterpolatorFactory, RenderNodeAnimator |
| Color | Color, ColorSpace |
| Other | Interpolator, Gainmap, DrawFilter, Camera, YuvImage |
Source evidence:
DefaultNativeRuntimeLoader.javalines 73–121.
7. Shadow → Native Delegation Flow
┌──────────────────────────────────────────────────────────────────┐
│ Test code: Paint paint = new Paint(); │
│ │
│ 1. Android Paint constructor calls native method nInit() │
│ │
│ 2. Instrumented nInit() → ClassHandler → ShadowWrangler │
│ │
│ 3. ShadowWrangler finds ShadowNativePaint │
│ → @Implementation nInit() method exists: │
│ a) Calls DefaultNativeRuntimeLoader.injectAndLoad() │
│ b) Ensures native library is loaded │
│ c) Delegates to PaintNatives.nInit() │
│ │
│ 4. PaintNatives.nInit() is a Java native method │
│ → JNI resolves to $$robo$$nInit$nativeBinding │
│ → Calls C++ Paint_init() in libhwui's jni/Paint.cpp │
│ → Creates real SkPaint object via Skia │
│ → Returns native pointer as jlong │
│ │
│ For V+ with callNativeMethodsByDefault = true: │
│ If no explicit shadow method exists, ShadowWrangler │
│ routes directly to $$robo$$<method>$nativeBinding │
└──────────────────────────────────────────────────────────────────┘
7.1 Shadow Example: ShadowNativePaint
@Implements(value = Paint.class, minSdk = O, callNativeMethodsByDefault = true)
public class ShadowNativePaint {
@Implementation(minSdk = O, maxSdk = U.SDK_INT)
protected static long nInit() {
DefaultNativeRuntimeLoader.injectAndLoad(); // Ensure library loaded
ShadowNativeTypeface.ensureInitialized(); // Dependency init
return PaintNatives.nInit(); // Delegate to real native
}
// For V+, most methods use callNativeMethodsByDefault
// and don't need explicit shadow implementations
}
Source evidence:
shadows/framework/.../ShadowNativePaint.javalines 36–50.
7.2 Shadow Example: ShadowNativeSQLiteConnection
@Implements(className = "android.database.sqlite.SQLiteConnection",
callNativeMethodsByDefault = true)
public class ShadowNativeSQLiteConnection extends ShadowSQLiteConnection {
@Implementation(minSdk = O_MR1, maxSdk = U.SDK_INT)
protected static long nativeOpen(String path, int openFlags, ...) {
DefaultNativeRuntimeLoader.injectAndLoad();
return PerfStatsCollector.getInstance()
.measure("androidsqlite",
() -> SQLiteConnectionNatives.nativeOpen(path, openFlags, ...));
}
}
Source evidence:
shadows/framework/.../ShadowNativeSQLiteConnection.javalines 46–66.
8. Resource Loading & Initialization
8.1 Native Library Loading Flow
DefaultNativeRuntimeLoader.ensureLoaded()
│
├── 1. Check isSupported() — verify OS/arch
├── 2. Create TempDirectory("nativeruntime")
├── 3. maybeCopyFonts() — extract fonts from JAR to temp dir
│ └── Sets: robolectric.nativeruntime.fontdir=/tmp/.../fonts/
├── 4. maybeCopyIcuData() — extract ICU data file
│ └── Sets: icu.data.path=/tmp/.../icu/icudt68l.dat
│ └── Sets: icu.locale.default=<default locale tag>
├── 5. [V+ only] Set system properties:
│ ├── core_native_classes=android.database.CursorWindow,...
│ ├── graphics_native_classes=android.graphics.Bitmap,...
│ └── method_binding_format=$$robo$$${method}$nativeBinding
├── 6. loadLibrary() — extract .so/.dylib/.dll and System.load()
│ └── Looks up: native/<os>/<arch>/libandroid_runtime.so
├── 7. [V+ only] invokeDeferredStaticInitializers()
│ └── Calls __staticInitializer__ on classes that need
│ deferred init after JNI registration
└── 8. [V+ only] Typeface.loadPreinstalledSystemFontMap()
Source evidence:
DefaultNativeRuntimeLoader.javalines 176–218.
8.2 Library Path Convention
Native libraries are stored in JAR resources at:
native/<os>/<arch>/<library_name>
Examples:
native/linux/x86_64/libandroid_runtime.so
native/mac/x86_64/libandroid_runtime.dylib
native/mac/aarch64/libandroid_runtime.dylib
native/windows/x86_64/libandroid_runtime.dll
Source evidence:
DefaultNativeRuntimeLoader.javalines 332–334.
8.3 Bundled Resources
The nativeruntime module includes these runtime resources:
| Resource | Purpose |
|---|---|
fonts/*.ttf |
System fonts (100+ font files including Roboto, Noto families) |
fonts/fonts.xml |
Font family configuration |
icu/icudt68l.dat |
ICU data (internationalization, date/number formatting, collation) |
arsc/ |
Android compiled resource data |
9. AOSP Build Integration (Soong)
9.1 Module Dependency Graph
Robolectric_all (java_library_host)
├── Robolectric_nativeruntime (java_library_host)
│ ├── robolectric_nativeruntime_native_prebuilt (pre-V prebuilt .so/.dylib)
│ ├── Robolectric_sandbox
│ ├── Robolectric_resources
│ └── Robolectric_pluginapi
├── Robolectric_shadows_framework (shadows layer)
├── Robolectric_robolectric
├── robolectric-host-android_all (java_library)
│ ├── robolectric_android-all-device-deps (framework JARs)
│ │ ├── framework-all, core-libart-for-host, services, ...
│ ├── libandroid_runtime_jar (native .so in JAR for V+)
│ │ └── :libandroid_runtime (cc_library_shared, host)
│ ├── robolectric_framework_res
│ ├── robolectric_props_jar (build.prop via robolectric.go)
│ └── icu-data_host_robolectric
└── ...
9.2 android_robolectric_test Module Type
Platform tests use the android_robolectric_test Soong module type which:
- Adds
Robolectric_allas a runtime dependency - Sets up the classpath with
robolectric-host-android_all - Runs tests on the host JVM with the native library available
9.3 robolectric_build_props
A custom Soong module (soong/robolectric.go) generates a build.prop file with
platform-specific properties (SDK version, codename, etc.) that Robolectric reads at runtime:
// soong/robolectric.go
"ro.build.version.sdk=" + ctx.Config().PlatformSdkVersion().String(),
"ro.build.version.release=" + ctx.Config().PlatformVersionName(),
"ro.build.version.codename=" + ctx.Config().PlatformSdkCodename(),
Source evidence:
soong/robolectric.golines 42–86.
10. JNI_OnLoad & Native Entry Point
When System.load() loads the native library, JNI_OnLoad in HostRuntime.cpp runs:
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void*) {
JNIEnv* env = nullptr;
vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6);
// Check if base host runtime should be initialized
string useBaseHostRuntime = getJavaProperty(env, "use_base_native_hostruntime", "true");
if (useBaseHostRuntime == "true") {
Vector<String8> args;
HostRuntime runtime;
runtime.onVmCreated(env); // Store JavaVM reference
runtime.start("HostRuntime", args, false); // Register JNI + init
}
return JNI_VERSION_1_6;
}
HostRuntime::start() triggers:
setJniMethodFormat()— readsmethod_binding_formatproperty for method renamingstartReg()→register_android_core_classes()+register_android_graphics_classes()onStarted()→loadIcuData(),property_initialize_ro_cpu_abilist(), set locale
Source evidence:
frameworks/base/core/jni/platform/host/HostRuntime.cpplines 296–306.
11. Version Evolution (Pre-V vs V+)
| Aspect | Pre-V (≤ API 34) | V+ (API 35+) |
|---|---|---|
| Library name | librobolectric-nativeruntime.{so,dylib,dll} |
libandroid_runtime.{so,dylib,dll} |
| Source | Built externally, shipped as prebuilt (nativeruntime-dist-compat) |
Built from AOSP source via Soong |
| JNI registration | Implicit via native method names | Explicit class-name-driven with system properties |
| Method binding | Standard JNI naming | Renamed via $$robo$$${method}$nativeBinding pattern |
| Shadow style | Explicit @Implementation for each native method |
callNativeMethodsByDefault = true — most methods auto-delegate |
| Static initializers | N/A | Deferred via invokeDeferredStaticInitializers() |
Source evidence:
DefaultNativeRuntimeLoader.javalines 336–343, 387–389.
12. Shadow Inventory
There are 88 ShadowNative* files in shadows/framework/src/main/java/.../shadows/,
covering these major subsystems:
| Subsystem | Key Shadows | Native Delegate |
|---|---|---|
| Graphics Core | ShadowNativeBitmap, ShadowNativeBitmapFactory, ShadowNativeCanvas, ShadowNativeBaseCanvas, ShadowNativeRecordingCanvas | BitmapNatives, BitmapFactoryNatives, CanvasNatives, BaseCanvasNatives, RecordingCanvasNatives |
| Drawing | ShadowNativePaint, ShadowNativeShader, ShadowNativeColorFilter, ShadowNativePathEffect, ShadowNativeMaskFilter | PaintNatives, ShaderNatives, ColorFilterNatives, PathEffectNatives, MaskFilterNatives |
| Geometry | ShadowNativePath, ShadowNativePathMeasure, ShadowNativeMatrix, ShadowNativeRegion, ShadowNativePathIterator | PathNatives, PathMeasureNatives, MatrixNatives, RegionNatives, PathIteratorNatives |
| Text/Font | ShadowNativeTypeface, ShadowNativeFont, ShadowNativeFontFamily, ShadowNativeLineBreaker, ShadowNativeMeasuredText | TypefaceNatives, FontBuilderNatives, FontFamilyNatives, LineBreakerNatives, MeasuredTextNatives |
| Render Pipeline | ShadowNativeRenderNode, ShadowNativeHardwareRenderer, ShadowNativeRenderEffect | RenderNodeNatives, HardwareRendererNatives, RenderEffectNatives |
| Animation | ShadowNativeAnimatedVectorDrawable, ShadowNativeVectorDrawable, ShadowNativeRenderNodeAnimator | AnimatedVectorDrawableNatives, VectorDrawableNatives, RenderNodeAnimatorNatives |
| Database | ShadowNativeSQLiteConnection, ShadowNativeSQLiteRawStatement, ShadowNativeCursorWindow | SQLiteConnectionNatives, CursorWindowNatives |
| Image | ShadowNativeImageDecoder, ShadowNativeImageReader, ShadowNativeAnimatedImageDrawable | ImageDecoderNatives, ImageReaderNatives, AnimatedImageDrawableNatives |
| Color | ShadowNativeColor, ShadowNativeColorSpaceRgb, ShadowNativeBlendModeColorFilter | ColorNatives, ColorSpaceRgbNatives, BlendModeColorFilterNatives |
| Surface | ShadowNativeSurface | SurfaceNatives |
| Resources | ShadowNativeApkAssets, ShadowNativeAssetManager, ShadowNativeStringBlock, ShadowNativeXmlBlock | (via libandroidfw) |
13. Compiled Native Library Dependency Tree
The following is a summary of major native libraries statically linked into the host
libandroid_runtime.so and their roles:
libandroid_runtime.so (host shared library)
│
├── HostRuntime.cpp — JNI_OnLoad, AndroidRuntime host impl
├── android_database_SQLiteConnection.cpp — SQLite JNI bindings
├── android_database_CursorWindow.cpp — CursorWindow JNI
├── android_view_Surface.cpp — Surface JNI
├── android_animation_PropertyValuesHolder.cpp — Animation JNI
├── com_android_internal_util_VirtualRefBasePtr.cpp — RefBase JNI
│
├── libhwui (static, compiled with HWUI_NULL_GPU)
│ ├── LayoutlibLoader.cpp / jni_runtime.cpp — Graphics JNI registration
│ ├── jni/Paint.cpp, jni/Bitmap.cpp, jni/Canvas.cpp, ... — All graphics JNI
│ ├── hwui/Bitmap.cpp, hwui/Canvas.cpp, hwui/Typeface.cpp — Core graphics impl
│ ├── pipeline/skia/SkiaCpuPipeline.cpp — CPU-only rendering
│ └── platform/host/* — Host-specific stubs
│
├── libskia (static) — Google's 2D graphics engine
├── libminikin (static) — Android text layout engine
├── libharfbuzz_ng (static) — Unicode text shaping
├── libft2 (static) — FreeType font rasterization
├── libandroidfw (static) — Android framework (resources, assets)
├── libsqlite (static) — SQLite database engine
├── libpng (static) — PNG codec
├── libjpeg (static) — JPEG codec
├── libwebp-decode/encode (static) — WebP codec
├── libz (static) — zlib compression
├── libhostgraphics (static) — Host stubs for ANativeWindow, Fence, etc.
├── libicu{i18n,uc} (static) — ICU internationalization
├── libbase (static) — Android base/logging
├── libcutils (static) — Android core utilities
├── liblog (static) — Android logging
└── libutils (static) — Android utility library
14. The nativeruntime-dist-compat Maven Artifact
For external (non-AOSP) Gradle builds, Robolectric distributes prebuilt native libraries as
the Maven artifact org.robolectric:nativeruntime-dist-compat (current version: 1.0.17).
This JAR contains pre-compiled native shared libraries for all supported platforms:
native/linux/x86_64/librobolectric-nativeruntime.so
native/mac/x86_64/librobolectric-nativeruntime.dylib
native/mac/aarch64/librobolectric-nativeruntime.dylib
native/windows/x86_64/robolectric-nativeruntime.dll
It is used in the Gradle build via:
// nativeruntime/build.gradle.kts
implementation(libs.robolectric.nativeruntime.dist.compat)
The CI workflow .github/workflows/graphics_tests.yml runs graphics tests on all four
platform configurations (Linux x86_64, macOS x86_64, macOS arm64, Windows x86_64).
Source evidence:
gradle/libs.versions.tomlline 2, 186.
15. Some AOSP Changes for Native Runtime
15.1 Thematic Analysis
Phase 1: Foundation (Jan–Apr 2022)
The first wave of changes created the entire build infrastructure from scratch:
-
Build target creation —
I19296fc4destablished the initialrobolectric_native_runtimebuild target inframeworks/base/core/jni/Android.bp, acc_library_sharedwithhost_supported: true. This single CL (+164 lines) laid the foundation for the entire effort. -
Distribution pipeline —
Ieb351f5badded the library to$DIST_DIR/robolectric/nativeruntime, andI5a99f9f7similarly distributed the ICU dat file required for text processing. - Core JNI classes — A rapid series of CLs incrementally added JNI registration for the
most critical Android framework classes:
I9661646a: Matrix and initial graphics JNII046e1447: Bitmap and BitmapFactory — enabling image decodingI6071aa4d: FontFamily with namespace handling (com.android.internal.graphics.fontsvsandroid.graphics.fontsfor different SDK levels)I369181f0: Typeface and text classes — completing text layout support
- Compatibility guard —
I7aab27c7added an SDK-level check to only register graphics JNI for SDK >= 26 (Android O), avoiding crashes from missingMatrixJNI fields in older versions.
Phase 2: libnativehelper and ICU (May–Sep 2022)
-
libnativehelper adaptation (
Icaef0cb0) — Changed the Buffer internals accessor class from a LayoutLib class toorg.robolectric.nativeruntime.NIOAccess, and enabled a staticcc_libraryfor the host build. This was essential becauselibnativehelperprovides core JNI helper functions used throughout the native runtime. -
ICU locale property (
Ia219a6c2) — Updated the system property name used for ICU’s default language tag, ensuring proper locale handling in host text operations.
Phase 3: macOS Support (Jan 2023)
Ic51c46c8(“Add support for building RNG on Mac”, +75/-57) was a significant change that:- Updated the
robolectric_native_runtimetarget with macOS-specific build rules - Excluded
AnimatedImageDrawablefrom Mac builds (requires Linux’sepoll) - Included
CursorWindow.cppfor Mac (SQLite cursor support) - Validated with
ShadowNativeMatrixTeston Mac artifacts
- Updated the
Phase 4: Windows Platform (Dec 2023 – Mar 2024)
The Windows porting effort required changes across multiple AOSP repositories:
-
Ie932ddec(“Enable host Windows build”) — Initial Windows build target. SQLite JNI was initially disabled becauseCursorWindow.cppand SQLite JNI neededmmap/ashmem. -
Ie72a5f04(“ashmem-host for Windows”, system/core) — Migrated totmpfile/filenofor temp file operations (supported on MinGW), enabling CursorWindow on Windows. -
I138064847(“SQLite JNI in Windows”) — Used libbase’sMappedFilefor cross-platform mmap operations inCursorWindow.cppandandroid_database_SQLiteConnection.cpp. -
I38b67e6f(“AnimatedImageDrawable for Mac and Windows”) — Re-enabledAnimatedImageDrawableon Mac/Windows by disabling theAnimatedEndListenermechanism (which requires native Looper, unavailable on host).
Two changes landed on AOSP main to improve portability for all host builds:
I77b6b548— Replaced rawmmap/munmapcalls withMappedFileinCursorWindowI535449c2— Same replacement inandroid_database_SQLiteConnection
Phase 5: ICU and Locale Improvements (Jan–Mar 2024)
I7c0ffffe— Changed the ICU language tag property fromrobolectric.nativeruntime.languageTagtoicu.locale.defaultfor alignment with upstream LayoutLib.I894b9b79(+84/-18) — Backported the latest LayoutLib ICU loading logic to the hostruntime-dev branch, improving internationalization support.I21e71152— Used the minimal"C"numeric locale instrtofcalls to prevent float parsing failures in locales where commas are decimal separators (e.g., German locale).
Phase 6: Hardware Rendering and Surface Support (May–Jul 2024)
This phase added hardware-accelerated rendering support (via CPU-only Skia pipeline):
Ic037fea1— Fixed Mac pixel color swap issue by usingkN32_SkColorType(architecture- dependent) instead of a hardcoded color type forSkiaHostPipeline.I949382eb— ExtendedImageReaderandHardwareRenderer.syncAndDrawFramesupport to Android Q and R (previously only Android S+).If52fe2a4(+46/-44) — Synchronized hwui files withsc-layoutlib-nativebranch, enablingCanvasContext,RootRenderNode,RenderThread, animation logic, and more.I74a7d09e— Fixed hwui compile errors som libhwuiworks with the Android toolchain (not just the host toolchain).Ieca45f62— Un-ifdef’dSurface.lockCanvasandSurface.unlockCanvasAndPost(they compile fine on host).I22038e78— Addedandroid_view_ThreadedRenderer_setSurfacePtrfor backwards compatibility with Android O/P.I1631d08d— Fixed frameinfo data conversion from the 9-length (Android R) to 12-length (Android S+) format for proper HW rendering support.Ic80c6aef— Added FileDescriptor-basedImageDecoder.nDecodevariant.
Phase 7: Path APIs and Newer SDK Support (Aug 2024 – Jan 2026)
Iab0c43c7(+84) — BackportedPathIteratorJNI from Android U.Ib980365a(+74/-40) — AddednConicTo,nRConicTo,nInterpolate,nGetGenerationID, andnIsInterpolatablefrom Android U’s Path API.I217e91ae— Added a host-specific variant ofPathIterator.nNextJNI.I96475c4b(+12) — Added unexported symbol list for Mac to prevent symbol clashes betweenlibrobolectric-nativeruntime(pre-V) andlibandroid_runtime(V+) forANativeWindowandMinikinsymbols.I19e69cd5(+45/-2, Jul 2025) — Addedandroid.text.HyphenatorJNI support with a configurable hyphen data directory via system property.I8a533353(+3/-1, Jan 2026) — BoundBitmapRegionDecoderJNI for region-based image decoding.
15.2 Development Branch Strategy
android12-hostruntime-dev (main development branch)
│
│ Most RNR-specific changes land here first
│ Based on Android 12 codebase
│ Contains customizations that can't go to main
│ (host-only ifdef's, backported APIs, etc.)
│
├── Changes stay here: JNI additions, host-only adaptations
│
└── Portable improvements → cherry-picked to main
│
▼
main (AOSP trunk)
Only gets changes that benefit ALL host builds
(LayoutLib + Robolectric)
e.g., MappedFile portability improvements
The android12-hostruntime-dev branch serves as a long-running development branch where
RNR-specific modifications are made independently of AOSP main. This approach is necessary
because:
-
Many changes are host-only — They add
#ifdef __ANDROID__guards, host-specific code paths, or backported APIs that don’t belong in the shipping Android platform. -
The base is Android 12 — The native runtime needs to support older SDK levels (O through R) with a stable codebase, while main continuously evolves.
-
Select improvements get upstreamed — Portable changes (like
MappedFilefor mmap) that benefit both Robolectric and LayoutLib are cherry-picked to AOSP main.
15.3 Cross-Repository Impact
The changes touch 6 AOSP repositories, demonstrating the breadth of modifications needed:
| Repository | # Changes | Purpose |
|---|---|---|
platform/frameworks/base |
~30+ | Core JNI, hwui, SQLite, graphics — the bulk of changes |
platform/libnativehelper |
1 | Buffer access class adaptation for Robolectric |
platform/frameworks/native |
1 | libnativewindow headers for Windows |
platform/system/core |
2 | ashmem-host Windows support, OWNERS |
platform/external/icu |
1 | ICU data distribution |
platform/manifest |
1 | Branch mapping for system/core |
15.4 Key Engineering Patterns
-
Incremental JNI binding — Rather than a single massive CL, classes were added in small groups (Matrix → Bitmap → Font → Typeface), each tested independently.
-
Platform-first, then broaden — Linux was first, then Mac (Jan 2023), then Windows (Dec 2023), with each platform bringing unique challenges (epoll, mmap, symbol visibility).
-
Upstream when possible — Portable improvements (MappedFile) go to
main; host-only customizations stay onandroid12-hostruntime-dev. -
API backporting — Newer APIs (PathIterator from U, Hyphenator) are backported to the hostruntime-dev branch so RNR can support them across all SDK levels.
-
Shared infrastructure with LayoutLib — Many patterns (ICU loading, locale handling, JNI method format) are shared with Android Studio’s LayoutLib, reducing duplication.
16. Summary
Robolectric’s Native Runtime achieves high-fidelity Android emulation on the JVM by:
-
Compiling real AOSP C++ libraries (libhwui, libskia, libsqlite, libminikin, etc.) for host platforms (Linux/macOS/Windows) with GPU support disabled (
HWUI_NULL_GPU) -
Packaging them as a single shared library (
libandroid_runtime.so) that statically links all dependencies -
Using bytecode instrumentation to redirect native method calls through a renaming scheme (
$$robo$$<method>$nativeBinding) that allows the shadow layer to intercept calls when needed while defaulting to real native execution -
Providing 88+ shadow classes that ensure correct initialization, lazy loading of the native library, and version-specific API compatibility across Android O through V+
-
Bundling runtime resources (fonts, ICU data, build properties) that the native code requires for correct operation
This approach delivers pixel-accurate graphics rendering, real SQLite behavior, and authentic text layout in Robolectric tests, significantly improving test fidelity compared to the older shadow-only approach.