whoami
Lucas Jenß
cat /etc/motd
The Coding Journal ツ — Notes taken on an epic coding journey. Technical solutions, debugging notes, and practical guides from the trenches of software development.
ls -la ~/languages/
- drwxr-xr-x
- ▶ PHP
- ▶ Ruby
- ▶ Scala
- ▶ C#
- ▶ JavaScript
- ▶ Objective-C
- ▶ Shell Scripting
ls -la ~/toolchain/
- drwxr-xr-x
- ▶ Typo3
- ▶ Akka
- ▶ Capistrano
- ▶ Git
- ▶ MAMP
- ▶ Adobe Illustrator
- ▶ NSTrackingArea (Cocoa)
uname -a
- drwxr-xr-x
- ▶ Mac OS X
- ▶ Unix
Using Android NDK With Kotlin And C++
Kotlin is usually the sensible starting point for Android application code, but some projects still need native C++. Existing image-processing libraries, audio engines, computer-vision routines and hardware integrations may already be written in C++. Rebuilding that code in Kotlin can be expensive, risky or technically impractical.
The Android Native Development Kit (NDK) provides the bridge. Kotlin calls a small JNI-facing function, the native layer performs its work in C++, and the result returns through a carefully defined interface. This arrangement gives an Android app access to native performance without forcing the entire application into C++.
The important work is at the boundary between managed and native code. Build configuration, ABI selection, memory ownership, string conversion and error handling all affect reliability. A clean boundary makes the integration easier to test on a Pixel device in Melbourne, an emulator in Sydney, or a production handset used anywhere in Australia.
Why Put Native Code Behind Kotlin
The NDK is useful when native code already exists or when a workload benefits from predictable low-level performance. An image filter processing camera frames, a cryptography library, a game engine and a specialist scientific algorithm are common examples. It is rarely worthwhile to move ordinary Android screens, networking or business rules into C++ simply because native code is available.
Kotlin should normally remain responsible for application behaviour, lifecycle events and Android framework APIs. C++ can focus on a narrow task such as transforming a buffer or calculating a result. This division keeps the Android side familiar while allowing the native side to use established libraries and efficient data structures.
There is a cost to every JNI call. Passing thousands of individual values across the boundary is slower and harder to maintain than passing one array or a direct buffer. A useful design groups work into meaningful operations. For example, one call that processes an entire audio block is preferable to a call for every sample.
Native code also has different failure modes. A memory leak, invalid pointer or uncaught native exception can terminate the process rather than produce a recoverable Kotlin exception. Treating the interface as a small public API, with documented input ranges and ownership rules, prevents many difficult crashes.
Setting Up The NDK Build
Android Studio can create a project with C++ support, but an existing Kotlin application can add native code through CMake and externalNativeBuild. The module needs an NDK version, a CMake configuration and a native source directory. Pinning versions in Gradle is useful because a colleague in Brisbane should receive the same build as a developer working from Adelaide.
A minimal Kotlin declaration might look like this:
class NativeProcessor {
external fun invertPixels(input: ByteArray): ByteArray
companion object {
init {
System.loadLibrary("nativeprocessor")
}
}
}
The library name passed to System.loadLibrary excludes the lib prefix and .so suffix. If the CMake target is called nativeprocessor, Android loads the generated native library using that name.
The corresponding CMake file can define the shared library and link Android’s logging library:
cmake_minimum_required(VERSION 3.22.1)
project("nativeprocessor")
add_library(nativeprocessor SHARED nativeprocessor.cpp)
find_library(log-lib log)
target_link_libraries(nativeprocessor ${log-lib})
The Gradle configuration should specify supported ABIs deliberately. arm64-v8a is the normal choice for modern physical phones, while x86_64 is useful for current emulators. Including every ABI increases the application size and can expose native dependencies that were built for only one architecture.
Building A Reliable JNI Boundary
The native function must use the JNI types expected by the Kotlin declaration. For a class method, the generated signature includes JNIEnv*, a jobject receiver and the method arguments. A simplified implementation for the example above is:
#include <jni.h>
extern "C"
JNIEXPORT jbyteArray JNICALL
Java_com_example_NativeProcessor_invertPixels(
JNIEnv* env, jobject, jbyteArray input) {
const jsize length = env->GetArrayLength(input);
jbyteArray output = env->NewByteArray(length);
if (output == nullptr) {
return nullptr;
}
jbyte* values = env->GetByteArrayElements(input, nullptr);
if (values == nullptr) {
env->DeleteLocalRef(output);
return nullptr;
}
env->SetByteArrayRegion(output, 0, length, values);
env->ReleaseByteArrayElements(input, values, JNI_ABORT);
return output;
}
The exported name depends on the package and class name, so refactoring a Kotlin package can break a manually named JNI function. Registering native methods with JNI_OnLoad avoids long names and makes the relationship explicit. For small experiments, the conventional name is acceptable, but larger projects benefit from explicit registration.
Use extern "C" to prevent C++ name mangling. A missing declaration, incorrect package path or mismatched parameter type commonly produces UnsatisfiedLinkError. The Kotlin method and native signature must agree on arrays, primitive values, nullable results and static versus instance methods.
Data ownership deserves particular attention. A ByteArray copied into native memory is straightforward but may cost time for large images or video frames. Direct ByteBuffer objects can reduce copying, although they require careful capacity checks and a clear lifetime. Never retain a pointer obtained from JNI after releasing it, and never return a pointer to memory owned by a temporary C++ object.
Handling Errors And Debugging Crashes
Native exceptions must not cross the JNI boundary. Catch C++ exceptions inside the exported function and translate them into a Java exception with env->ThrowNew. Kotlin can then handle a meaningful failure instead of receiving an abrupt process termination. Validate array lengths, dimensions and numeric ranges before passing them into an algorithm.
Logging is valuable during development. Android’s logcat, native stack traces and debugger breakpoints can reveal whether a failure occurs while loading the library, entering JNI or executing the C++ routine. Sanitizers such as AddressSanitizer and UndefinedBehaviorSanitizer can identify use-after-free and buffer errors earlier than a customer report.
The same disciplined approach applies to older systems code. A useful example is this guide to debugging a PHP memory error, where the visible failure is only a symptom of what happens underneath. In an Android app, inspect the native allocation and ownership path rather than increasing limits blindly.
Test on a physical device as well as an emulator. An x86_64 emulator may behave differently from an arm64 phone, and a native library that works on a developer workstation may fail when packaged for the Play Store. Test release builds too: compiler optimisation can expose undefined behaviour hidden by a debug build.
Managing Performance Across Environments
JNI overhead is usually small compared with substantial native computation, but it becomes visible in tight loops. Batch work, reuse buffers where safe and avoid repeated conversion between Kotlin strings and C++ strings. For camera or audio pipelines, measure frame time, allocation frequency and dropped frames rather than relying on intuition.
The Android profiler can show CPU usage and allocations, while native tools help inspect C++ hotspots. Benchmark with representative data: a small sample on a fast development phone says little about a sustained workload on an older handset used on a regional Australian network or during a long commute.
Threading needs a clear contract. A native function called from the main thread must return quickly, or the interface should dispatch work to a background executor or coroutine. If C++ starts its own worker threads, ensure JNI access uses a properly attached thread and that global references remain valid for the required lifetime.
Practices Worth Keeping In The Project
- Pin the NDK and CMake versions, then build every supported ABI in continuous integration.
- Keep JNI functions thin, with validation at the boundary and algorithmic work in ordinary testable C++ classes.
- Document who owns buffers, strings and native objects, including when each resource is released.
- Test debug, release and minified builds on both an emulator and a physical arm64 device.
- Capture native crash symbols so production failures can be mapped back to source lines.
A small native wrapper is easier to review than a broad layer exposing dozens of C++ classes to Kotlin. Prefer stable operations such as processFrame, decodeFile or calculateFeatures, and keep Android-specific concerns outside the C++ core. This structure also makes unit testing possible without launching an Android activity.
For applications sold or distributed in Australia, release validation should include the actual packaging path used for Google Play, including App Bundles and device-specific delivery. Check that every native dependency is included for the target market’s common devices and that a 64-bit build is available. An EOFY release deadline is a poor time to discover that only the emulator ABI was packaged.
Start with one narrow function, verify its inputs and outputs, then expand the interface only when profiling justifies it. Build the Kotlin caller, CMake target and C++ implementation together, inspect logcat when loading fails, and run the same tests against debug and release variants. That workflow turns Android NDK integration from a fragile experiment into a maintainable part of the application.
cat ~/interests.json
| Key | Value |
|---|---|
| editor | Terminal-first workflow |
| os | Mac OS X / Unix |
| vcs | Git, distributed version control |
| deploy | Capistrano, cron automation |
| graphics | SVG, Adobe Illustrator troubleshooting |
| networking | IP validation, SSH, VPN |
git log --oneline --reverse
Solving SVG import issues in Adobe Illustrator CS6 and CC
When importing an SVG into Illustrator, the operation fails with an unknown error [CANT]. A workaround for this Adobe-side bug.
Solving NDK build issues on OS X
Troubleshooting native development kit compilation problems on Mac OS X.
Programmatically adding PHP generated TypoScript to the backend configuration
Integrating dynamically generated TypoScript into Typo3 backend setups using PHP.
ArgumentError: Could not parse PKey: no start line
Debugging an SSH key parsing error encountered during deployment.
Validating IP-Addresses in PHP
Using PHP filter functions with flags like FILTER_FLAG_IPV4 and FILTER_FLAG_IPV6, and understanding how filter_var handles reserved IP addresses.
Cocoa: Using NSTrackingArea
A short tutorial on using Cocoa's NSTrackingArea to capture mouseEntered and mouseExited events.
cat ~/contact.txt