Skip to main content

Native ABIs, memory safety, and debugging

An ABI defines instruction set, calling convention, binary format, alignment, and related contracts. Android packages shared libraries under lib/<abi>/libname.so; missing or incompatible libraries can install yet fail when loaded.

Delivery design​

Choose supported ABIs from user/device/store requirements. Test Play-generated splits and every shipped ABI. Ensure every native dependency has a compatible library; a transitive prebuilt .so can constrain the matrix. Track native size by ABI and strip release symbols only after preserving symbol files for exact builds.

Memory and concurrency hazards​

Native code adds use-after-free, buffer overflow, data race, integer overflow, double-free, and ownership ambiguity beyond managed-memory risks. Prefer RAII/smart pointers, bounded spans/containers, checked size arithmetic, immutable ownership contracts, and fuzz/property tests for parsers.

std::vector<std::uint8_t> decode(std::span<const std::uint8_t> input) {
if (input.size() > kMaxPayload) throw InputError("payload too large");
// Validate structure before allocating based on untrusted lengths.
return decodeValidated(input);
}

Use CheckJNI in debug, sanitizers on supported test builds, native unit/fuzz tests, debugger, tombstones, and exact unstripped symbols/build IDs. A Java crash reporter without native symbol upload produces weak evidence.

Performance reality​

Measure JNI marshalling, copies, startup/library loading, CPU, thermal throttling, battery, binary size, and maintainability. A faster inner loop can produce a slower product if crossing/copying dominates.

Practical mistakes​

  • ❌ Support only the developer emulator ABI.
  • ✅ test every packaged/store-delivered ABI.
  • ❌ Strip symbols and discard them.
  • ✅ archive symbols keyed to artifact/build ID and verify symbolication.
  • ❌ Allocate from untrusted native length fields before bounds checks.
  • ✅ validate size/overflow before allocation and parsing.

Source​