SweetLoader is the native runtime that maps into the Minecraft Bedrock process and exposes hook,
signature, memory and input primitives to the Glowberry Client launcher and to native mods. It is
the engine behind CaramelClient's .universe and .singularity mod formats.
It is a shared library, not an app. The launcher's MinecraftActivity calls
System.loadLibrary("sweetloader"), which maps it into the game's own process — that is the point:
a mod's hooks must run in the address space of libminecraftpe.so.
- Signature resolution. Finds a byte pattern or a vtable slot inside a stripped game binary, where no symbol exists. Game classes export no dynamic symbols, so this is the only route to an address.
- Hooking. Installs an in-place trampoline (
pl::memory::hook) through the vendored GlossHook backend, with a prologue verification so a stale pattern cannot corrupt code. - Input bridging. Receives touch, key, text and mouse events from the game and forwards them to
registered callbacks (the launcher's
PreloaderInput, and native mods). - Cosmetics rendering. Draws the equipped cape, hat and pet on the local player through the game's own player renderer.
- Mod Menu / HUD bridge. Supplies the in-game Mod Menu and HUD overlays their state.
include/pl/ public headers (the API a mod or the launcher includes)
src/pl/ implementation
memory/ signature resolution, hooking, patching, ELF helpers
runtime/ game hooks, cosmetics, input bridge, optifine mode
cosmetics/ geometry, animation, network
render/ GL probing, camera source
hooks/ the hook-manager facade
legacy/ the C entry points kept for older mods
lib/ARM64/ vendored prebuilt GlossHook static library
tests/ host tests (pure C++, no device needed)
docs/ per-area notes
The sweetloader is built as part of the client APK; you normally do not build it on its own. The
client's app/build.gradle adds app/src/main/cpp/sweetloader as a CMake subdirectory, and the
NDK toolchain compiles it for arm64-v8a.
To build it directly:
cmake -B build -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake \
-DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=28
cmake --build buildarm64-v8a is the only supported ABI. The client ships a single arm64 APK, and GlossHook is
vendored for arm64 only; a 32-bit configure is refused rather than silently producing a library
the client would never load.
Dependencies are fetched at configure time and pinned by commit in CMakeLists.txt (see NOTICE).
The two mod formats are backed by Rust crates under rust/, cross-compiled for arm64-v8a into
libcaramel_universe.so and libcaramel_singularity.so:
| Crate | Role |
|---|---|
universe-core |
.universe manifest, Ed25519 signing, permissions, ZIP reader/extractor, load order |
singularity-core |
.singularity world generation: noise, biomes, structures, mesher, fluids, physics |
universe-jni |
the .universe cdylib: JNI entry points + a C ABI the C++ bridge calls |
singularity-jni |
the .singularity cdylib |
caramel-cli |
packs a directory into a package, signs it, verifies it, keygens, inspects |
The C++ runtime reaches the loader through pl/engine/UniverseBridge.cpp, which resolves the
already-loaded cdylib's C ABI with RTLD_NOLOAD — it never loads a second copy.
Cross-compile (needs the NDK and an aarch64-linux-android Rust target):
bash rust/build-android.shRun the Rust tests on the host:
cd rust && cargo testThe pure parts — the geometry parser, animation solver, socket protocol, signature pattern parser,
ELF build-id reader, callback snapshot, the .universe bridge's fail-closed contract — carry no
Android types and are tested on the build host:
bash tests/cosmetics/run_tests.shThey need a host C++20 compiler and zlib headers. The nlohmann/json single header is fetched
into a gitignored cache on first run, or point NLOHMANN_JSON_INCLUDE at an existing copy.
The three ThreadSanitizer tests need setarch (util-linux) so the sanitizer can disable ASLR;
without it they report skipped rather than failing, and CI treats that as a failure so a
sanitizer that stopped running cannot read as a pass.
.github/workflows/ci.yml runs on every push and pull request, and a green run is the claim that
the library a client loads is the library the sources describe. Four jobs, each a real check:
| Job | What it proves |
|---|---|
host-tests |
The C++ suite (57 tests, including the TSAN ones) passes on the host, with no sanitizer silently skipped. |
rust |
cargo fmt --check, cargo clippy -D clippy::correctness and cargo test --workspace (249 tests) are clean on a pinned toolchain. |
native-build |
libsweetloader.so and the three Rust cdylibs cross-compile for arm64-v8a on the NDK the client pins, and the built .so exports the JNI and SDK symbols with no static-archive leakage. |
repo-integrity |
No .cpp is missing from SWEETLOADER_SOURCES, no host test is unregistered, every fetched dependency is pinned to a commit and attributed in NOTICE, and the tooling's own tests pass. |
The two drift checks are also runnable by hand:
python3 scripts/check_sources_listed.py
python3 scripts/check_pinned_deps.py
python3 tools/tests/gen_rules_test.pygen_rules.py derives the RTTI vtable rules from a Minecraft APK. Its test builds a synthetic
stripped-binary-shaped ELF so the .data.rel.ro relocation step is exercised without a real
multi-hundred-megabyte download; disabling that step makes three of the eight tests fail.
The Rust tree was normalized with rustfmt when the fmt --check gate was added. It had never
been formatted, so the gate is what keeps it that way.
A mod is a shared library that the client's mod loader dlopens. It includes the public headers
under include/pl/ and links against the sweetloader, which is already resident in the process:
#include "pl/memory/Signature.hpp"
#include "pl/memory/Hook.hpp"
static int (*originalTick)(void*) = nullptr;
static int hookTick(void* self) { /* ... */ return originalTick(self); }
void mod_init() {
auto target = pl::memory::resolveSignature(".. ?? ..", "libminecraftpe.so");
if (target) {
pl::memory::hook(reinterpret_cast<pl::memory::FuncPtr>(target),
reinterpret_cast<pl::memory::FuncPtr>(hookTick),
reinterpret_cast<pl::memory::FuncPtr*>(&originalTick),
pl::memory::HookPriority::Normal, "mod.tick");
}
}Notes that matter:
- A signature is valid for one build only. A game update relinks the binary and the pattern stops matching; check the return value and fail closed rather than hooking a wrong address. The resolver keys its cache by the module's ELF build id for exactly this reason.
hookis the one exported signature. The name is a parameter, not a default argument, so the symbol a mod links against is the symbol the library exports.- Hook callbacks run on the game's own threads. They must not block, allocate per frame, or
take a lock that a render path also takes. See
docs/player-render-hook.md. - Only
arm64-v8ais supported, and the game must be a build the signature rules cover.
The public surface is the C++ headers under include/pl/, plus the C entry points in
src/pl/legacy/. A mod that links against the sweetloader is coupled to that surface: an exported
function's signature or a header's layout must not change without a version bump and a note here.
The pl::memory::hook signature is the clearest example — it is a parameter list, not a default
argument, so that a mod built against a different arity fails to load loudly rather than binding to
the wrong function.
docs/universe-format.md— the.universemod format: manifest, signature, permissions, lifecycle.docs/singularity-format.md— the.singularityworld-gen format: manifest, determinism, pipeline.docs/player-render-hook.md— the player-render seam the cosmetics renderer uses.docs/mce-image-pipeline.md— the engine image-loader seam.docs/native-cosmetic-render-route.md— why the engine-geometry route is not reachable.docs/resource-pack-reload.md— why an in-place pack reload is not implemented.docs/sweetloader-item-status.md— per-item status of the hardening work.
Example packs live under examples/ (example_universe, sky_islands), each with the manifest and
sources to build it.
Apache-2.0. See LICENSE. Third-party components and their licences are listed in NOTICE.