Ndk
Idiomatic Go bindings for 34 Android NDK modules — Camera2, AAudio, OpenGL ES, Vulkan, sensors, MediaCodec, and more.
Install / Use
/learn @AndroidGoLab/NdkREADME
ndk
Idiomatic Go bindings for the Android NDK, auto-generated from C headers to ensure full coverage and easy maintenance.
Android Interfaces for Go
This project is part of a family of three Go libraries that cover the major Android interface surfaces. Each wraps a different layer of the Android platform:
graph TD
subgraph "Go application"
GO["Go code"]
end
subgraph "Interface libraries"
NDK["<b>ndk</b><br/>C API bindings via cgo"]
JNI["<b>jni</b><br/>Java API bindings via JNI+cgo"]
AIDL["<b>binder</b><br/>Binder IPC, pure Go"]
end
subgraph "Android platform"
CAPI["NDK C libraries<br/>(libcamera2ndk, libaaudio,<br/>libEGL, libvulkan, ...)"]
JAVA["Java SDK<br/>(android.bluetooth,<br/>android.location, ...)"]
BINDER["/dev/binder<br/>kernel driver"]
SYSSVCS["System services<br/>(ActivityManager,<br/>PowerManager, ...)"]
end
GO --> NDK
GO --> JNI
GO --> AIDL
NDK -- "cgo / #include" --> CAPI
JNI -- "cgo / JNIEnv*" --> JAVA
AIDL -- "ioctl syscalls" --> BINDER
BINDER --> SYSSVCS
JAVA -. "internally uses" .-> BINDER
CAPI -. "some use" .-> BINDER
| Library | Interface | Requires | Best for | | ------------------------------------------------------------- | ---------------------------- | ------------------- | --------------------------------------------------------------------------------------------------- | | ndk (this project) | Android NDK C APIs | cgo + NDK toolchain | High-performance hardware access: camera, audio, sensors, OpenGL/Vulkan, media codecs | | jni | Java Android SDK via JNI | cgo + JNI + JVM/ART | Java-only APIs with no NDK equivalent: Bluetooth, WiFi, NFC, location, telephony, content providers | | binder | Binder IPC (system services) | pure Go (no cgo) | Direct system service calls without Java: works on non-Android Linux with binder, minimal footprint |
When to use which
-
Start with ndk when the NDK provides a C API for what you need (camera, audio, sensors, EGL/Vulkan, media codecs). These are the lowest-latency, lowest-overhead bindings since they go straight from Go to the C library via cgo.
-
Use jni when you need a Java Android SDK API that the NDK does not expose. Examples: Bluetooth discovery, WiFi P2P, NFC tag reading, location services, telephony, content providers, notifications. JNI is also the right choice when you need to interact with Java components (Activities, Services, BroadcastReceivers) or when you need the gRPC remote-access layer.
-
Use binder when you want pure-Go access to Android system services without any cgo dependency. This is ideal for lightweight tools, CLI programs, or scenarios where you want to talk to the binder driver from a non-Android Linux system. AIDL covers the same system services that Java SDK wraps (ActivityManager, PowerManager, etc.) but at the wire-protocol level.
-
Combine them when your application needs multiple layers. For example, a streaming app might use ndk for camera capture and audio encoding, jni for Bluetooth controller discovery, and binder for querying battery status from a companion daemon.
How they relate to each other
All three libraries talk to the same Android system services, but through different paths:
- The NDK C APIs are provided by Google as stable C interfaces to Android platform features. Some (camera, sensors, audio) internally use binder IPC to talk to system services; others (EGL, Vulkan, OpenGL) talk directly to kernel drivers. The
ndklibrary wraps these C APIs via cgo. - The Java SDK uses binder IPC internally for system service access (BluetoothManager, LocationManager, etc.), routing calls through the Android Runtime (ART/Dalvik). The
jnilibrary calls into these Java APIs via the JNI C interface and cgo. - The AIDL binder protocol is the underlying IPC mechanism that system-facing NDK and Java SDK APIs use. The
binderlibrary implements this protocol directly in pure Go, bypassing both C and Java layers entirely.
Requirements
- Android NDK r28 (28.0.13004108) or later
- API level 35 (Android 15) target
Idiomatic vs capi/ Packages
Always import the idiomatic top-level packages (github.com/AndroidGoLab/ndk/{module}) in your application code. These provide Go-friendly types with proper lifecycle management (Close(), defer), typed error handling, and method receivers.
The capi/ packages (github.com/AndroidGoLab/ndk/capi/{module}) are the raw CGo bindings generated in Stage 2 of the pipeline. They mirror the C API directly — C-style function names, unsafe.Pointer parameters, raw integer return codes. They are intended for power users who need access to NDK functions not yet wrapped by the idiomatic layer. All commonly used functions — including hwbuf.Allocate, buf.Lock, codec.DequeueInputBuffer, and codec.DequeueOutputBuffer — are available in the idiomatic layer.
import "github.com/AndroidGoLab/ndk/hwbuf"
// Allocate a hardware buffer using the idiomatic API
desc := hwbuf.Desc{
Width: 1920, Height: 1080, Layers: 1,
Format: uint32(hwbuf.R8g8b8a8Unorm),
Usage: uint64(hwbuf.CpuWriteOften | hwbuf.GpuSampledImage),
}
buf, err := hwbuf.Allocate(&desc)
if err != nil {
log.Fatal(err)
}
defer buf.Close()
Examples
All types implement idempotent, nil-safe Close() error. Error types wrap NDK status codes and work with errors.Is.
package main
import (
"log"
"unsafe"
"github.com/AndroidGoLab/ndk/audio"
)
func main() {
builder, err := audio.NewStreamBuilder()
if err != nil {
log.Fatal(err)
}
defer builder.Close()
builder.
SetDirection(audio.Output).
SetSampleRate(44100).
SetChannelCount(2).
SetFormat(audio.PcmFloat).
SetPerformanceMode(audio.LowLatency).
SetSharingMode(audio.Shared)
stream, err := builder.Open()
if err != nil {
log.Fatal(err)
}
defer stream.Close()
log.Printf("opened: %d Hz, %d ch, burst=%d",
stream.SampleRate(), stream.ChannelCount(), stream.FramesPerBurst())
if err := stream.Start(); err != nil {
log.Fatal(err)
}
defer stream.Stop()
buf := make([]float32, int(stream.FramesPerBurst())*2)
stream.Write(unsafe.Pointer(&buf[0]), stream.FramesPerBurst(), 1_000_000_000)
}
</details>
<details>
<summary>Camera discovery</summary>
package main
import (
"log"
"github.com/AndroidGoLab/ndk/camera"
)
func main() {
mgr := camera.NewManager()
defer mgr.Close()
ids, err := mgr.CameraIdList()
if err != nil {
log.Fatal(err) // camera.ErrPermissionDenied if CAMERA not granted
}
for _, id := range ids {
meta, _ := mgr.GetCameraCharacteristics(id)
orientation := meta.I32At(uint32(camera.SensorOrientation), 0)
log.Printf("camera %s: orientation=%d°", id, orientation)
}
}
</details>
<details>
<summary>Sensor querying</summary>
package main
import (
"fmt"
"github.com/AndroidGoLab/ndk/sensor"
)
func main() {
mgr := sensor.GetInstance()
accel := mgr.DefaultSensor(sensor.Accelerometer)
fmt.Printf("Sensor: %s (%s)\n", accel.Name(), accel.Vendor())
fmt.Printf("Resolution: %g, min delay: %d µs\n",
accel.Resolution(), accel.MinDelay())
}
</details>
<details>
<summary>Event loop (ALooper)</summary>
package main
import (
"log"
"runtime"
"time"
"unsafe"
"github.com/AndroidGoLab/ndk/looper"
)
func main() {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
lp := looper.Prepare(int32(looper.ALOOPER_PREPARE_ALLOW_NON_CALLBACKS))
defer func() { _ = lp.Close() }()
lp.Acquire()
go func() {
time.Sleep(100 * time.Millisecond)
lp.Wake()
}()
var fd, events int32
var data unsafe.Pointer
result := looper.LOOPER_POLL(looper.PollOnce(-1, &fd, &events, &data))
switch result {
case looper.ALOOPER_POLL_WAKE:
log.Println("woke up")
case looper.ALOOPER_POLL_TIMEOUT:
log.Println("timed out")
}
}
</details>
<details>
<summary>Camera preview (full pipeline)</summary>
A complete camera-to-screen example using NativeActivity, EGL, and OpenGL ES. See examples/camera/display/ for the full working application. Build it with make apk-displaycamera.
// Sketch of the camera pipeline (requires NativeActivity context)
mgr := camera.NewManager()
defer mgr.Close()
device, err := mgr.OpenCamera(cameraID, camera.DeviceStateCallbacks{
OnDisconnected: func() { log.Println("disconnected") },
OnError: func(code int) { log.Printf("error: %d", code) },
})
defer device.Close()
request, _ := device.CreateCaptureRequest(camera.Preview)
defer request.Close()
target, _ := camera.NewOutputTarget(nativeWindow)
request.AddTarget(target)
container, _ := camera.NewSessionOutputContainer()
output, _ := camera.NewSessionOutput(nativeWindow)
container.Add(output)
session, _ := device.CreateCaptureSession(container,
camera.SessionStateCallbacks{
OnReady: func() { log.Println("ready") },
OnActive: func() { log.Println("active") },
})
session.Set
