How Rust code is shipped in an Android app
Rust compiles to a shared object per ABI and the build places it in the same library directory as any other native code. A crate built as a dynamic library becomes that object, and the application loads it from Java with a library load call and calls methods declared native. The Java layer therefore sees a small set of entry points while the rest of the crate stays internal.
A build can also arrive through a binding framework. A generator such as UniFFI or a bridge tool defines its own exported functions and its own data conversion code, which adds a predictable set of generated names to the dynamic symbol table. Recognizing that pattern separates generated scaffolding from application logic, and the scaffolding is usually where the type conversions happen.
How name mangling affects symbol reading
Rust mangles function names so that they carry the crate, the module path and any generic parameters. An older mangled name begins with a prefix and ends with a hash suffix, and the current scheme uses a different prefix and encodes the path as nested segments. Neither form reads as English on its own.
Demangle with a tool that understands Rust, because a C++ demangler leaves Rust names partially decoded and the remaining segments look like noise. After demangling, the crate and module path gives the structure of the library, and a function that sits in a signing or auth module stands out immediately.
Only functions marked for export appear in the dynamic symbol table. A release build can strip the rest, which removes internal names but leaves the exported entry points together with panic locations and message strings. A stripped Rust library is still workable because the export list and the string table carry the shape of the code, and the module path in a panic message recovers names that stripping removed.
Where the JNI boundary is declared
- A function exported with a Java_ prefix names the Java class and method directly, which gives the mapping without any registration step.
- A library that registers its methods exports the load entry point and passes a table that maps Java method names to function pointers, so the Java names sit as strings in the initialization routine.
- A generated binding layer exports its own entry points and dispatches by name, so the Java method name appears as a string argument rather than in the symbol.
- A length-prefixed string table holds literals without terminators, and the code loads the pointer and the length together at the reference site.
The Java_ form is the fastest to read because the class and method names are already in the symbol. The registration form costs one extra step, since the method names are strings and the function pointers sit in a table that a disassembler renders as an array of addresses.
How string handling and error paths differ from Java or C++
A Rust string is a pointer and a length and it is not terminated by a zero byte. Plain string extraction still finds the bytes, but adjacent literals can run together, so read the length operand at the reference site to separate them. A string that crosses the foreign function boundary is usually converted to a terminated form, which is why some literals do appear with a terminator and others do not.
Error handling leaves readable traces. A panic records a message and a source location, so the library contains file paths such as a source directory with a module name, plus messages from unwrap or expect calls. Those strings name the module that failed and often the condition that failed, which is more informative than a Java exception thrown without a stack trace.
The network path depends on the crate. A Rust HTTP client built on hyper or curl performs its own I/O, so Java interceptors see nothing and the request is visible only at the socket or TLS layer, where a hook on the write function reveals the plaintext. A crate that returns a request description to Java leaves the transfer in the Java stack. Certificate verification inside a Rust TLS stack leaves distinctive error strings that name the exact check that failed, and native Android reverse engineering and APK to API cover both cases.
Related work
Reviewed 28 September 2026 · SReverse research desk