Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

History

107 Commits

Repository files navigation

ndk

Go ReferenceGo Report CardLicense: MITGo VersionAsk AI

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
Loading
LibraryInterfaceRequiresBest for
ndk (this project)Android NDK C APIscgo + NDK toolchainHigh-performance hardware access: camera, audio, sensors, OpenGL/Vulkan, media codecs
jniJava Android SDK via JNIcgo + JNI + JVM/ARTJava-only APIs with no NDK equivalent: Bluetooth, WiFi, NFC, location, telephony, content providers
binderBinder 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 ndk library 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 jni library 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 binder library 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 APIdesc:= hwbuf.Desc{
Width: 1920, Height: 1080, Layers: 1,
Format: uint32(hwbuf.R8g8b8a8Unorm),
Usage: uint64(hwbuf.CpuWriteOften|hwbuf.GpuSampledImage),
}
buf, err:=hwbuf.Allocate(&desc)
iferr!=nil {
log.Fatal(err)
}
deferbuf.Close()

Examples

All types implement idempotent, nil-safe Close() error. Error types wrap NDK status codes and work with errors.Is.

Audio playback (AAudio)
package main
import (
"log""unsafe""github.com/AndroidGoLab/ndk/audio"
)
funcmain() {
builder, err:=audio.NewStreamBuilder()
iferr!=nil {
log.Fatal(err)
}
deferbuilder.Close()
builder.
SetDirection(audio.Output).
SetSampleRate(44100).
SetChannelCount(2).
SetFormat(audio.PcmFloat).
SetPerformanceMode(audio.LowLatency).
SetSharingMode(audio.Shared)
stream, err:=builder.Open()
iferr!=nil {
log.Fatal(err)
}
deferstream.Close()
log.Printf("opened: %d Hz, %d ch, burst=%d",
stream.SampleRate(), stream.ChannelCount(), stream.FramesPerBurst())
iferr:=stream.Start(); err!=nil {
log.Fatal(err)
}
deferstream.Stop()
buf:=make([]float32, int(stream.FramesPerBurst())*2)
stream.Write(unsafe.Pointer(&buf[0]), stream.FramesPerBurst(), 1_000_000_000)
}
Camera discovery
package main
import (
"log""github.com/AndroidGoLab/ndk/camera"
)
funcmain() {
mgr:=camera.NewManager()
defermgr.Close()
ids, err:=mgr.CameraIdList()
iferr!=nil {
log.Fatal(err) // camera.ErrPermissionDenied if CAMERA not granted
}
for_, id:=rangeids {
meta, _:=mgr.GetCameraCharacteristics(id)
orientation:=meta.I32At(uint32(camera.SensorOrientation), 0)
log.Printf("camera %s: orientation=%d°", id, orientation)
}
}
Sensor querying
package main
import (
"fmt""github.com/AndroidGoLab/ndk/sensor"
)
funcmain() {
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())
}
Event loop (ALooper)
package main
import (
"log""runtime""time""unsafe""github.com/AndroidGoLab/ndk/looper"
)
funcmain() {
runtime.LockOSThread()
deferruntime.UnlockOSThread()
lp:=looper.Prepare(int32(looper.ALOOPER_PREPARE_ALLOW_NON_CALLBACKS))
deferfunc() { _=lp.Close() }()
lp.Acquire()
gofunc() {
time.Sleep(100*time.Millisecond)
lp.Wake()
}()
varfd, eventsint32vardata unsafe.Pointerresult:=looper.LOOPER_POLL(looper.PollOnce(-1, &fd, &events, &data))
switchresult {
caselooper.ALOOPER_POLL_WAKE:
log.Println("woke up")
caselooper.ALOOPER_POLL_TIMEOUT:
log.Println("timed out")
}
}
Camera preview (full pipeline)

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()
defermgr.Close()
device, err:=mgr.OpenCamera(cameraID, camera.DeviceStateCallbacks{
OnDisconnected: func() { log.Println("disconnected") },
OnError: func(codeint) { log.Printf("error: %d", code) },
})
deferdevice.Close()
request, _:=device.CreateCaptureRequest(camera.Preview)
deferrequest.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.SetRepeatingRequest(request)
Asset loading
package main
import (
"fmt""io""unsafe""github.com/AndroidGoLab/ndk/asset"
)
funcmain() {
// mgr obtained from activity.AssetManager in a real NativeActivity app.// This example documents the API pattern.varmgr*asset.Manager// = activity.AssetManager(nativeActivity)a:=mgr.Open("textures/wood.png", asset.Streaming)
defera.Close()
size:=a.Length()
buf:=make([]byte, size)
_, _=io.ReadFull(unsafe.NewReader(a), buf)
fmt.Printf("read %d bytes\n", len(buf))
}
How to record from the microphone
package main
import (
"log""math""time""unsafe""github.com/AndroidGoLab/ndk/audio"
)
funcmain() {
builder, err:=audio.NewStreamBuilder()
iferr!=nil {
log.Fatalf("create stream builder: %v", err)
}
deferbuilder.Close()
builder.
SetDirection(audio.Input).
SetSampleRate(48000).
SetChannelCount(1).
SetFormat(audio.PcmI16).
SetPerformanceMode(audio.LowLatency).
SetSharingMode(audio.Shared)
stream, err:=builder.Open()
iferr!=nil {
log.Fatalf("open stream: %v", err)
}
deferfunc() {
iferr:=stream.Close(); err!=nil {
log.Printf("close stream: %v", err)
}
}()
rate:=stream.SampleRate()
log.Printf("capture stream opened (rate=%d Hz, ch=%d)", rate, stream.ChannelCount())
iferr:=stream.Start(); err!=nil {
log.Fatalf("start stream: %v", err)
}
// Read approximately 1 second of audio.totalFrames:=ratebuf:=make([]int16, 1024)
bufBytes:=unsafe.Slice((*byte)(unsafe.Pointer(&buf[0])), len(buf)*int(unsafe.Sizeof(buf[0])))
varcaptured []int16forint32(len(captured)) <totalFrames {
framesToRead:=int32(len(buf))
ifremaining:=totalFrames-int32(len(captured)); remaining<framesToRead {
framesToRead=remaining
}
n, err:=stream.Read(bufBytes, framesToRead, time.Second)
iferr!=nil {
log.Fatalf("read: %v", err)
}
captured=append(captured, buf[:n]...)
}
iferr:=stream.Stop(); err!=nil {
log.Fatalf("stop stream: %v", err)
}
// Compute peak amplitude.varpeakint16for_, s:=rangecaptured {
ifs<0 {
s=-s
}
ifs>peak {
peak=s
}
}
log.Printf("captured %d frames", len(captured))
log.Printf("peak amplitude: %d (%.1f dBFS)", peak, 20*math.Log10(float64(peak)/32767.0))
log.Println("recording example finished")
}
How to take a picture from the camera
package main
import (
"log""os""time""unsafe""github.com/AndroidGoLab/ndk/camera"
capimedia "github.com/AndroidGoLab/ndk/capi/media""github.com/AndroidGoLab/ndk/media"
)
funcmain() {
// 1. Create an ImageReader (640x480 JPEG, up to 2 images).// The idiomatic media.NewImageReader uses an output parameter.varreader*media.ImageReaderifstatus:=media.NewImageReader(640, 480, capimedia.AIMAGE_FORMAT_JPEG, 2, &reader); status!=0 {
log.Fatalf("create image reader: status %d", status)
}
deferreader.Close()
// 2. Get the ANativeWindow from the ImageReader.// The idiomatic Window() method has a broken signature (no output),// so we call the capi function directly.readerPtr:= (*capimedia.AImageReader)(reader.Pointer())
varcapiWindow*capimedia.ANativeWindowifstatus:=capimedia.AImageReader_getWindow(readerPtr, &capiWindow); status!=0 {
log.Fatalf("get window: status %d", status)
}
// Convert capi/media.ANativeWindow to capi/camera.ANativeWindow via unsafe.camWindow:= (*camera.ANativeWindow)(unsafe.Pointer(capiWindow))
// 3. Create camera Manager and list cameras.mgr:=camera.NewManager()
defermgr.Close()
ids, err:=mgr.CameraIDList()
iferr!=nil {
log.Fatalf("list cameras: %v", err)
}
iflen(ids) ==0 {
log.Fatal("no cameras available")
}
log.Printf("cameras: %v (using %s)", ids, ids[0])
// 4. Open the first camera.dev, err:=mgr.OpenCamera(ids[0], camera.DeviceStateCallbacks{
OnDisconnected: func() { log.Println("camera disconnected") },
OnError: func(codeint) { log.Printf("camera error: %d", code) },
})
iferr!=nil {
log.Fatalf("open camera: %v", err)
}
deferdev.Close()
// 5. Create OutputTarget and SessionOutput from the window.target, err:=camera.NewOutputTarget(camWindow)
iferr!=nil {
log.Fatalf("create output target: %v", err)
}
defertarget.Close()
sessOutput, err:=camera.NewSessionOutput(camWindow)
iferr!=nil {
log.Fatalf("create session output: %v", err)
}
defersessOutput.Close()
container, err:=camera.NewSessionOutputContainer()
iferr!=nil {
log.Fatalf("create session output container: %v", err)
}
defercontainer.Close()
iferr:=container.Add(sessOutput); err!=nil {
log.Fatalf("add session output: %v", err)
}
// 6. Create a CaptureRequest (StillCapture template) and add the target.req, err:=dev.CreateCaptureRequest(camera.StillCapture)
iferr!=nil {
log.Fatalf("create capture request: %v", err)
}
deferreq.Close()
req.AddTarget(target)
// 7. Create a CaptureSession and set a repeating request.ready:=make(chanstruct{}, 1)
session, err:=dev.CreateCaptureSession(container, camera.SessionStateCallbacks{
OnReady: func() { select { caseready<-struct{}{}: default: } },
OnActive: func() { log.Println("session active") },
OnClosed: func() { log.Println("session closed") },
})
iferr!=nil {
log.Fatalf("create capture session: %v", err)
}
defersession.Close()
iferr:=session.SetRepeatingRequest(req); err!=nil {
log.Fatalf("set repeating request: %v", err)
}
// Wait for at least one frame to arrive.select {
case<-ready:
case<-time.After(5*time.Second):
log.Println("warning: timed out waiting for session ready")
}
time.Sleep(500*time.Millisecond)
// 8. Acquire an image from the ImageReader and save it.varcapiImage*capimedia.AImageifstatus:=capimedia.AImageReader_acquireLatestImage(readerPtr, &capiImage); status!=0 {
log.Fatalf("acquire image: status %d", status)
}
img:=media.NewImageFromPointer(unsafe.Pointer(capiImage))
deferimg.Close()
varnumPlanesint32iferr:=img.NumberOfPlanes(&numPlanes); err!=nil {
log.Fatalf("get number of planes: %v", err)
}
// For JPEG there is exactly one plane; get its data.vardataPtr*uint8vardataLenint32ifstatus:=capimedia.AImage_getPlaneData(
(*capimedia.AImage)(img.Pointer()), 0, &dataPtr, &dataLen,
); status!=0 {
log.Fatalf("get plane data: status %d", status)
}
data:=unsafe.Slice(dataPtr, dataLen)
iferr:=os.WriteFile("/sdcard/capture.jpg", data, 0644); err!=nil {
log.Fatalf("write file: %v", err)
}
iferr:=session.StopRepeating(); err!=nil {
log.Printf("stop repeating: %v", err)
}
log.Printf("saved %d bytes to /sdcard/capture.jpg", dataLen)
}
How to list available sensors
package main
import (
"fmt""github.com/AndroidGoLab/ndk/sensor"
)
// printSensor queries and prints sensor properties. It returns false// if the sensor's underlying C pointer is NULL (the device lacks this// sensor type), recovering from the resulting panic.funcprintSensor(mgr*sensor.Manager, labelstring, sensorType sensor.Type) (okbool) {
deferfunc() {
ifr:=recover(); r!=nil {
ok=false
}
}()
s:=mgr.DefaultSensor(sensorType)
// Trigger a method call; if the internal pointer is NULL the NDK// dereferences a null pointer and Go's signal handler turns it// into a panic that we recover above.name:=s.Name()
vendor:=s.Vendor()
ifname==""||vendor=="" {
returnfalse
}
fmt.Printf(" %s:\n", label)
fmt.Printf(" Name: %s\n", name)
fmt.Printf(" Vendor: %s\n", vendor)
fmt.Printf(" Type: %s (%d)\n", sensorType, int32(sensorType))
fmt.Printf(" Resolution: %g\n", s.Resolution())
fmt.Printf(" Min delay: %d us\n", s.MinDelay())
fmt.Println()
returntrue
}
funcmain() {
mgr:=sensor.GetInstance()
typesensorInfostruct {
labelstringsensorType sensor.Type
}
sensors:= []sensorInfo{
{"Accelerometer", sensor.Accelerometer},
{"Gyroscope", sensor.Gyroscope},
{"Light", sensor.Light},
{"Proximity", sensor.Proximity},
{"Magnetic Field", sensor.MagneticField},
}
fmt.Println("Default sensors on this device:")
fmt.Println()
found:=0for_, info:=rangesensors {
ifprintSensor(mgr, info.label, info.sensorType) {
found++
} else {
fmt.Printf(" %-16s not available\n", info.label+":")
}
}
iffound==0 {
fmt.Println(" No default sensors found on this device.")
}
}
How to check device thermal status
package main
import (
"fmt""github.com/AndroidGoLab/ndk/thermal"
)
funcmain() {
mgr:=thermal.NewManager()
defermgr.Close()
status:=mgr.CurrentStatus()
fmt.Printf("Thermal status: %s (%d)\n", status, int32(status))
switchstatus {
casethermal.StatusNone:
fmt.Println("Device is cool.")
casethermal.StatusLight, thermal.StatusModerate:
fmt.Println("Device is warm; consider reducing workload.")
casethermal.StatusSevere, thermal.StatusCritical:
fmt.Println("Device is hot; throttling likely.")
casethermal.StatusEmergency, thermal.StatusShutdown:
fmt.Println("Device is critically hot; shutdown imminent.")
default:
fmt.Println("Unable to determine thermal status.")
}
}
How to query GPU capabilities
package main
import (
"fmt""log""unsafe""github.com/AndroidGoLab/ndk/egl""github.com/AndroidGoLab/ndk/gles2"
)
// goString converts a *gles2.GLubyte (C string) to a Go string.funcgoString(p*gles2.GLubyte) string {
ifp==nil {
return"<nil>"
}
// Walk the null-terminated byte sequence.varbuf []byteforptr:= (*byte)(unsafe.Pointer(p)); *ptr!=0; ptr= (*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(ptr)) +1)) {
buf=append(buf, *ptr)
}
returnstring(buf)
}
funcmain() {
// 1. Get the default EGL display and initialize it.dpy:=egl.GetDisplay(egl.EGLNativeDisplayType(0))
ifdpy==nil {
log.Fatal("eglGetDisplay failed")
}
varmajor, minor egl.Intifegl.Initialize(dpy, &major, &minor) ==egl.False {
log.Fatalf("eglInitialize failed: 0x%x", egl.GetError())
}
deferegl.Terminate(dpy)
fmt.Printf("EGL %d.%d\n", major, minor)
fmt.Printf(" Vendor: %s\n", egl.QueryString(dpy, egl.EGL_VENDOR))
fmt.Printf(" Version: %s\n", egl.QueryString(dpy, egl.EGL_VERSION))
fmt.Printf(" Client APIs: %s\n", egl.QueryString(dpy, egl.EGL_CLIENT_APIS))
fmt.Printf(" Extensions: %s\n", egl.QueryString(dpy, egl.EGL_EXTENSIONS))
// 2. Choose a config with ES2 support and pbuffer surface type.attribs:= []egl.Int{
egl.RenderableType, egl.OpenglEs2Bit,
egl.SurfaceType, egl.PbufferBit,
egl.RedSize, 8,
egl.GreenSize, 8,
egl.BlueSize, 8,
egl.None,
}
varcfg egl.EGLConfigvarnumCfg egl.Intifegl.ChooseConfig(dpy, &attribs[0], &cfg, 1, &numCfg) ==egl.False||numCfg==0 {
log.Fatal("eglChooseConfig failed")
}
// 3. Create a 1x1 pbuffer surface and an ES2 context.pbufAttribs:= []egl.Int{egl.Width, 1, egl.Height, 1, egl.None}
surface:=egl.CreatePbufferSurface(dpy, cfg, &pbufAttribs[0])
ctxAttribs:= []egl.Int{egl.ContextClientVersion, 2, egl.None}
ctx:=egl.CreateContext(dpy, cfg, nil, &ctxAttribs[0])
ifctx==nil {
log.Fatal("eglCreateContext failed")
}
deferegl.DestroyContext(dpy, ctx)
deferegl.DestroySurface(dpy, surface)
egl.MakeCurrent(dpy, surface, surface, ctx)
// 4. Query OpenGL ES strings.fmt.Println()
fmt.Printf("GL Vendor: %s\n", goString(gles2.GetString(gles2.GL_VENDOR)))
fmt.Printf("GL Renderer: %s\n", goString(gles2.GetString(gles2.GL_RENDERER)))
fmt.Printf("GL Version: %s\n", goString(gles2.GetString(gles2.GL_VERSION)))
fmt.Printf("GL Extensions: %s\n", goString(gles2.GetString(gles2.GL_EXTENSIONS)))
egl.MakeCurrent(dpy, nil, nil, nil)
}
How to probe available media codecs
package main
import (
"fmt""github.com/AndroidGoLab/ndk/media"
)
funcmain() {
codecs:= []struct {
mimestringdescstring
}{
{"video/avc", "H.264 / AVC"},
{"video/hevc", "H.265 / HEVC"},
{"video/x-vnd.on2.vp8", "VP8"},
{"video/x-vnd.on2.vp9", "VP9"},
{"video/av01", "AV1"},
{"audio/mp4a-latm", "AAC"},
{"audio/opus", "Opus"},
{"audio/flac", "FLAC"},
}
fmt.Printf("%-28s %-10s %-10s\n", "MIME Type", "Encoder", "Decoder")
fmt.Printf("%-28s %-10s %-10s\n", "---", "---", "---")
for_, c:=rangecodecs {
encOK:="no"enc:=media.NewEncoder(c.mime)
ifenc!=nil&&enc.Pointer() !=nil {
encOK="yes"enc.Close()
}
decOK:="no"dec:=media.NewDecoder(c.mime)
ifdec!=nil&&dec.Pointer() !=nil {
decOK="yes"dec.Close()
}
fmt.Printf("%-28s %-10s %-10s (%s)\n", c.mime, encOK, decOK, c.desc)
}
}
How to read device configuration
package main
import (
"fmt""github.com/AndroidGoLab/ndk/config"
)
funcmain() {
cfg:=config.NewConfig()
defercfg.Close()
fmt.Println("Device configuration:")
fmt.Printf(" Density: %d dpi\n", cfg.Density())
fmt.Printf(" Orientation: %d\n", cfg.Orientation())
fmt.Printf(" Screen size: %d\n", cfg.ScreenSize())
fmt.Printf(" Screen width: %d dp\n", cfg.ScreenWidthDp())
fmt.Printf(" Screen height: %d dp\n", cfg.ScreenHeightDp())
fmt.Printf(" SDK version: %d\n", cfg.SdkVersion())
switchconfig.Orientation(cfg.Orientation()) {
caseconfig.OrientationPort:
fmt.Println(" (portrait)")
caseconfig.OrientationLand:
fmt.Println(" (landscape)")
caseconfig.OrientationSquare:
fmt.Println(" (square)")
default:
fmt.Println(" (any/unknown)")
}
}
How to decode an image file
package main
import (
"fmt""log""syscall""unsafe"
capidec "github.com/AndroidGoLab/ndk/capi/imagedecoder""github.com/AndroidGoLab/ndk/image"
)
funcmain() {
// 1. Open the image file via POSIX fd.fd, err:=syscall.Open("/sdcard/photo.jpg", syscall.O_RDONLY, 0)
iferr!=nil {
log.Fatalf("open file: %v", err)
}
defersyscall.Close(fd)
// 2. Create a Decoder from the fd.// The factory function is not yet in the high-level image package,// so we call the capi function and wrap the result.vardecPtr*capidec.AImageDecoderifrc:=capidec.AImageDecoder_createFromFd(int32(fd), &decPtr); rc!=0 {
log.Fatalf("create decoder: error %d", rc)
}
decoder:=image.NewDecoderFromPointer(unsafe.Pointer(decPtr))
deferdecoder.Close()
// 3. Query image dimensions from the header.headerPtr:=capidec.AImageDecoder_getHeaderInfo(decPtr)
width:=capidec.AImageDecoderHeaderInfo_getWidth(headerPtr)
height:=capidec.AImageDecoderHeaderInfo_getHeight(headerPtr)
fmt.Printf("Image: %d x %d\n", width, height)
// 4. Query the stride and allocate the pixel buffer.stride:=decoder.MinimumStride()
bufSize:=stride*uint64(height)
pixels:=make([]byte, bufSize)
fmt.Printf("Stride: %d bytes, buffer: %d bytes\n", stride, bufSize)
// 5. Decode the image into the buffer.iferr:=decoder.Decode(unsafe.Pointer(&pixels[0]), stride, bufSize); err!=nil {
log.Fatalf("decode: %v", err)
}
fmt.Printf("Decoded %d bytes of RGBA pixel data.\n", bufSize)
}

For more examples, see examples/.

ndkcli

ndkcli is a CLI tool that exposes the full Android NDK surface from the command line. Supports 273 commands across 33 modules — query cameras, record audio, probe GPU capabilities, check sensors, and more.

Install

Option A: Download pre-built binary from GitHub Releases:

# Download for your device architecture
wget https://github.com/AndroidGoLab/ndk/releases/latest/download/ndkcli-android-arm64
adb push ndkcli-android-arm64 /data/local/tmp/ndkcli
adb shell chmod 755 /data/local/tmp/ndkcli

Option B: Build from source (requires Android NDK):

make ndkcli # → build/ndkcli (arm64)
adb push build/ndkcli /data/local/tmp/
adb shell chmod 755 /data/local/tmp/ndkcli

Quick start

# List cameras
adb shell /data/local/tmp/ndkcli camera list-details
# Record 5 seconds of audio
adb shell /data/local/tmp/ndkcli audio record --output /data/local/tmp/rec.pcm --duration 5s
# Check thermal status
adb shell /data/local/tmp/ndkcli thermal manager current-status
# Query GPU
adb shell /data/local/tmp/ndkcli gles2 info
# List sensors
adb shell /data/local/tmp/ndkcli sensor read --type 1

List all commands

# From source (no Android needed):
make ndkcli-commands
# On device:
adb shell /data/local/tmp/ndkcli --help
adb shell /data/local/tmp/ndkcli camera --help

Examples

List available cameras and their characteristics
# List camera IDs
adb shell /data/local/tmp/ndkcli camera manager camera-id-list
# Show full details (lens facing, orientation, hardware level) for all cameras
adb shell /data/local/tmp/ndkcli camera list-details
# Query characteristics for a specific camera
adb shell /data/local/tmp/ndkcli camera manager get-camera-characteristics --camera-id 0
Capture raw frames from the camera
# Capture 10 frames from camera 0 at 640x480 in RGBA format, save to file
adb shell /data/local/tmp/ndkcli camera capture \
--id 0 --width 640 --height 480 --format 1 --count 10 \
--output /data/local/tmp/frames.raw
Record audio from the microphone
# Record 5 seconds of mono 44.1kHz PCM16 audio
adb shell /data/local/tmp/ndkcli audio record \
--output /data/local/tmp/recording.pcm \
--duration 5s --sample-rate 44100 --channels 1
# Record 10 seconds of stereo 48kHz audio
adb shell /data/local/tmp/ndkcli audio record \
--output /data/local/tmp/stereo.pcm \
--duration 10s --sample-rate 48000 --channels 2
Play back recorded audio
# Play a previously recorded PCM file
adb shell /data/local/tmp/ndkcli audio play \
--input /data/local/tmp/recording.pcm \
--sample-rate 44100 --channels 1
Query audio system capabilities
# Open a probe stream and print audio properties
adb shell /data/local/tmp/ndkcli audio stream-builder new
adb shell /data/local/tmp/ndkcli audio stream channel-count
adb shell /data/local/tmp/ndkcli audio stream sample-rate
adb shell /data/local/tmp/ndkcli audio stream frames-per-burst
List sensors and read sensor data
# List all available sensors (probes known types)
adb shell /data/local/tmp/ndkcli sensor read --type 1 # Accelerometer
adb shell /data/local/tmp/ndkcli sensor read --type 4 # Gyroscope
adb shell /data/local/tmp/ndkcli sensor read --type 5 # Light# Query a specific sensor by type number
adb shell /data/local/tmp/ndkcli sensor manager default-sensor --value 1
adb shell /data/local/tmp/ndkcli sensor sensor name
adb shell /data/local/tmp/ndkcli sensor sensor vendor
adb shell /data/local/tmp/ndkcli sensor sensor resolution
Check thermal status
# One-shot thermal status
adb shell /data/local/tmp/ndkcli thermal manager current-status
# Monitor thermal status every 2 seconds for 30 seconds
adb shell /data/local/tmp/ndkcli thermal monitor --interval 2s --duration 30s
Query EGL and GPU capabilities
# EGL display information (vendor, version, extensions)
adb shell /data/local/tmp/ndkcli egl info
# List EGL configurations
adb shell /data/local/tmp/ndkcli egl configs
# OpenGL ES 2.0 info (creates pbuffer context, queries GL strings)
adb shell /data/local/tmp/ndkcli gles2 info
# OpenGL ES 3.0 info
adb shell /data/local/tmp/ndkcli gles3 info
Probe available media codecs
# Check which codecs are available (H.264, H.265, VP8/9, AV1, AAC, etc.)
adb shell /data/local/tmp/ndkcli media codecs
# Create specific encoder/decoder
adb shell /data/local/tmp/ndkcli media new-encoder --mime_type video/avc
adb shell /data/local/tmp/ndkcli media new-decoder --mime_type audio/mp4a-latm
# Probe a media file
adb shell /data/local/tmp/ndkcli media probe --file /sdcard/video.mp4
Read device configuration
# Show all configuration values (density, orientation, screen, SDK version)
adb shell /data/local/tmp/ndkcli config show
# Individual queries
adb shell /data/local/tmp/ndkcli config config density
adb shell /data/local/tmp/ndkcli config config sdk-version
adb shell /data/local/tmp/ndkcli config config screen-width-dp
adb shell /data/local/tmp/ndkcli config config orientation
Decode an image file
# Decode a JPEG/PNG and print dimensions, stride, format
adb shell /data/local/tmp/ndkcli image decode --file /sdcard/photo.jpg
# Decode with target size (downscale)
adb shell /data/local/tmp/ndkcli image decode --file /sdcard/photo.jpg --width 320 --height 240
Match system fonts
# Find a matching font by family name and weight
adb shell /data/local/tmp/ndkcli font match --family sans-serif --weight 400
adb shell /data/local/tmp/ndkcli font match --family serif --weight 700 --italic
Check permissions
# Check if a permission is granted for a PID/UID
adb shell /data/local/tmp/ndkcli permission check \
--name android.permission.CAMERA --pid 1000 --uid 1000
Trace and logging
# Check if tracing is enabled
adb shell /data/local/tmp/ndkcli trace is-enabled
# Add a trace marker
adb shell /data/local/tmp/ndkcli trace begin-section --section-name "my_operation"
adb shell /data/local/tmp/ndkcli trace end-section
# Set a trace counter
adb shell /data/local/tmp/ndkcli trace set-counter --counter-name "frames" --counter-value 42
# Write to Android log
adb shell /data/local/tmp/ndkcli log write --tag myapp --text "hello from ndkcli" --prio 4
NNAPI (Neural Networks) probe
# Check if NNAPI is available
adb shell /data/local/tmp/ndkcli nnapi probe
# Create and inspect a model
adb shell /data/local/tmp/ndkcli nnapi model new
Storage and OBB
# Check OBB mount status
adb shell /data/local/tmp/ndkcli storage obb --file /sdcard/main.obb
adb shell /data/local/tmp/ndkcli storage manager is-obb-mounted --filename /sdcard/main.obb
Looper and window utilities
# Test looper functionality (prepare, wake, poll)
adb shell /data/local/tmp/ndkcli looper test# Query window properties via ImageReader-backed window
adb shell /data/local/tmp/ndkcli window query

Supported Modules

NDK ModuleGo PackageImport Path
Graphics & Rendering
egleglgithub.com/AndroidGoLab/ndk/egl
gles2gles2github.com/AndroidGoLab/ndk/gles2
gles3gles3github.com/AndroidGoLab/ndk/gles3
vulkanvulkangithub.com/AndroidGoLab/ndk/vulkan
surfacecontrolsurfacecontrolgithub.com/AndroidGoLab/ndk/surfacecontrol
surfacetexturesurfacetexturegithub.com/AndroidGoLab/ndk/surfacetexture
hwbufhwbufgithub.com/AndroidGoLab/ndk/hwbuf
windowwindowgithub.com/AndroidGoLab/ndk/window
bitmapbitmapgithub.com/AndroidGoLab/ndk/bitmap
Camera & Imaging
cameracameragithub.com/AndroidGoLab/ndk/camera
imageimagegithub.com/AndroidGoLab/ndk/image
Audio & Media
audioaudiogithub.com/AndroidGoLab/ndk/audio
mediamediagithub.com/AndroidGoLab/ndk/media
midimidigithub.com/AndroidGoLab/ndk/midi
Sensors & Input
sensorsensorgithub.com/AndroidGoLab/ndk/sensor
inputinputgithub.com/AndroidGoLab/ndk/input
choreographerchoreographergithub.com/AndroidGoLab/ndk/choreographer
Activity & Lifecycle
activityactivitygithub.com/AndroidGoLab/ndk/activity
configconfiggithub.com/AndroidGoLab/ndk/config
thermalthermalgithub.com/AndroidGoLab/ndk/thermal
hinthintgithub.com/AndroidGoLab/ndk/hint
permissionpermissiongithub.com/AndroidGoLab/ndk/permission
Storage & Assets
assetassetgithub.com/AndroidGoLab/ndk/asset
storagestoragegithub.com/AndroidGoLab/ndk/storage
System & IPC
binderbindergithub.com/AndroidGoLab/ndk/binder
persistablebundlepersistablebundlegithub.com/AndroidGoLab/ndk/persistablebundle
looperloopergithub.com/AndroidGoLab/ndk/looper
logloggithub.com/AndroidGoLab/ndk/log
sharedmemsharedmemgithub.com/AndroidGoLab/ndk/sharedmem
syncsyncgithub.com/AndroidGoLab/ndk/sync
netnetgithub.com/AndroidGoLab/ndk/net
Machine Learning
nnapinnapigithub.com/AndroidGoLab/ndk/nnapi
Debugging & Fonts
tracetracegithub.com/AndroidGoLab/ndk/trace
fontfontgithub.com/AndroidGoLab/ndk/font

Architecture

The project converts Android NDK C headers into safe, idiomatic Go packages through three code generation stages. Each stage has a dedicated tool and its own set of input/output artifacts:

flowchart TD
NDK["Android NDK C Headers"]
MAN["capi/manifests/*.yaml"]
SPEC["spec/generated/{module}.yaml"]
CAPI["capi/{module}/*.go"]
OVER["spec/overlays/{module}.yaml"]
TMPL["templates/*.tmpl"]
IDOM["{module}/*.go"]
NDK -->|parsed by| S1
MAN -->|configures| S1
subgraph S1["Stage 1: specgen + c2ffi"]
direction LR
S1D["Extracts structured API spec"]
end
S1 --> SPEC
SPEC -->|read by| S2
MAN -->|configures| S2
subgraph S2["Stage 2: capigen"]
direction LR
S2D["Generates raw CGo bindings"]
end
S2 --> CAPI
SPEC --> S3
OVER -->|semantic annotations| S3
TMPL -->|code templates| S3
subgraph S3["Stage 3: idiomgen"]
direction LR
S3D["Generates idiomatic Go packages"]
end
S3 --> IDOM
style NDK fill:#e0e0e0,color:#000
style MAN fill:#fff3cd,color:#000
style OVER fill:#fff3cd,color:#000
style TMPL fill:#fff3cd,color:#000
style SPEC fill:#d4edda,color:#000
style CAPI fill:#d4edda,color:#000
style IDOM fill:#cce5ff,color:#000
Loading

Legend: Yellow = hand-written inputs, Green = generated intermediates, Blue = final output.

Stage 1: Spec Extraction (make specs)

Tool: tools/cmd/specgen + c2ffi (external)

Parses NDK C headers via c2ffi and extracts a structured YAML specification containing types, enums, functions, callbacks, and structs.

Input: capi/manifests/{module}.yaml + NDK sysroot headers

Output: spec/generated/{module}.yaml

The spec captures:

  • Types: opaque pointers, typedefs, pointer handles
  • Enums: constant groups with resolved values
  • Functions: signatures with parameter names and types
  • Callbacks: function pointer type signatures
  • Structs: field definitions

Example manifest (capi/manifests/looper.yaml):

GENERATOR:
PackageName: looperIncludes: ["android/looper.h"]FlagGroups:
- { name: LDFLAGS, flags: [-landroid] }

Example output (spec/generated/looper.yaml, abridged):

module: loopersource_package: github.com/AndroidGoLab/ndk/capi/loopertypes:
ALooper:
kind: opaque_ptrc_type: ALoopergo_type: "*C.ALooper"enums:
Looper_event_t:
- { name: ALOOPER_EVENT_INPUT, value: 1 }
- { name: ALOOPER_EVENT_OUTPUT, value: 2 }functions:
ALooper_prepare:
c_name: ALooper_prepareparams: [{ name: opts, type: int32 }]returns: "*ALooper"

Stage 2: Raw CGo Bindings (make capi)

Tool: tools/cmd/capigen (in-repo)

Reads the generated spec YAML and manifest YAML, then produces raw CGo wrapper packages with type aliases, function wrappers, callback proxies, and enum constants.

Input: spec/generated/{module}.yaml + capi/manifests/{module}.yaml

Output: capi/{module}/ -- a package with:

  • doc.go -- package declaration and doc comment
  • types.go -- Go type aliases (type ALooper C.ALooper)
  • const.go -- Enum constants
  • {module}.go -- Go functions that call through CGo
  • cgo_helpers.h / cgo_helpers.go -- Callback proxy declarations and implementations

Stage 3: Idiomatic Go Generation (make idiomatic)

Tool: tools/cmd/idiomgen (in-repo)

Merges the generated spec with a hand-written overlay and renders Go templates to produce the final user-facing packages.

Input:

  • spec/generated/{module}.yaml -- structured API spec
  • spec/overlays/{module}.yaml -- hand-written semantic annotations
  • templates/*.tmpl -- Go text templates

Output: {module}/*.go (e.g., looper/looper.go, looper/enums.go, ...)

flowchart LR
SPEC["spec/generated/{module}.yaml"]
OVER["spec/overlays/{module}.yaml"]
subgraph MERGE["Merge"]
direction TB
M1["Resolve type names"]
M2["Assign methods to receivers"]
M3["Classify enums (error vs value)"]
M4["Configure constructors/destructors"]
end
SPEC --> MERGE
OVER --> MERGE
MERGE --> MERGED["MergedSpec"]
TMPL["templates/*.tmpl"]
MERGED --> RENDER["Template Rendering"]
TMPL --> RENDER
RENDER --> PKG["package.go"]
RENDER --> TYPES["types.go"]
RENDER --> ENUMS["enums.go"]
RENDER --> ERRS["errors.go"]
RENDER --> FUNCS["functions.go"]
RENDER --> TF["<type>.go (per opaque type)"]
RENDER --> BRIDGE["capi/{module}/bridge_*.go"]
Loading

Overlay Files

Overlays provide semantic annotations that transform raw C APIs into idiomatic Go. They are the primary mechanism for customization and are entirely hand-written.

Example overlay (spec/overlays/looper.yaml):

module: looperpackage:
go_name: loopergo_import: github.com/AndroidGoLab/ndk/looperdoc: "Package looper provides Go bindings for Android ALooper."types:
ALooper:
go_name: Looper # ALooper -> Looperdestructor: ALooper_release # generates Close() methodpattern: ref_countedLooper_event_t:
go_name: Eventstrip_prefix: ALOOPER_ # ALOOPER_EVENT_INPUT -> EventInputfunctions:
ALooper_prepare:
go_name: Preparereturns_new: Looper # returns *Looper, acts as constructorALooper_wake:
receiver: Looper # becomes method: (*Looper).Wake()go_name: WakeALooper_release:
skip: true # handled by destructor, don't expose

Key overlay capabilities:

FeatureEffect
go_nameRename C symbol to idiomatic Go
go_error: trueEnum type implements error interface
constructor / destructorGenerate New*() and Close()
receiverTurn free function into method
returns_newMark function as factory returning new handle
strip_prefixRemove C naming prefix from enum constants
patternLifecycle pattern: builder, ref_counted, singleton
callback_structsGenerate CGo callback bridges with registry
struct_accessorsWrap C struct arrays as Go accessors
skip: trueExclude function from generation
chain: trueMethod returns receiver for chaining

Templates

13 Go text templates in templates/ control the shape of generated code:

TemplateGenerates
package.go.tmplPackage declaration and doc comment
type_alias_file.go.tmplType aliases for basic typedefs
value_enum_file.go.tmplType-safe enum definitions with String()
errors.go.tmplError type implementing error interface
functions.go.tmplPackage-level free functions
callback_file.go.tmplCallback type placeholders
type_file.go.tmplPer-opaque-type struct, constructor, destructor, methods
bridge_registry.go.tmplCallback registration via sync.Map
bridge_c.go.tmplC inline functions for callback dispatch
bridge_export.go.tmpl//export Go functions callable from C
lifecycle.go.tmplNativeActivity lifecycle API
bridge_lifecycle.go.tmplLifecycle callback C bridges
bridge_lifecycle_export.go.tmplLifecycle callback exports

End-to-End Example: looper

The transformation from C header to Go package:

flowchart TD
subgraph C["C Header (android/looper.h)"]
C1["ALooper* ALooper_prepare(int opts)"]
C2["void ALooper_wake(ALooper* looper)"]
C3["void ALooper_release(ALooper* looper)"]
end
subgraph SPEC["spec/generated/looper.yaml (specgen + c2ffi)"]
S1["types: ALooper: {kind: opaque_ptr}"]
S2["functions: ALooper_prepare: ..."]
end
subgraph CAPI["capi/looper/looper.go (capigen)"]
R1["type ALooper C.ALooper"]
R2["func ALooper_prepare(opts int32) *ALooper"]
R3["func ALooper_wake(looper *ALooper)"]
R4["func ALooper_release(looper *ALooper)"]
end
subgraph GO["looper/looper.go (idiomgen)"]
G1["type Looper struct { ptr *capi.ALooper }"]
G2["func Prepare(opts int32) *Looper"]
G3["func (h *Looper) Wake()"]
G4["func (h *Looper) Close() error"]
end
C --> SPEC
SPEC --> CAPI
SPEC --> GO
Loading

E2E Verification

Run make e2e-examples-test to test ndkcli commands on a connected device or emulator.

Verified platforms (click to expand)
TypeDeviceAndroidAPIABIBuildDatePassedTotal
PhonePixel 8a1636arm64-v8aBP4A.260205.0012026-03-151717
Emulatorsdk_gphone64_x86_641535x86_64Pixel_7_API_352026-03-151717

Project Layout

.
├── Makefile # Build orchestration
├── tools/
│ ├── cmd/
│ │ ├── specgen/ # Stage 1: spec extraction via c2ffi
│ │ ├── capigen/ # Stage 2: CGo wrapper generator
│ │ ├── idiomgen/ # Stage 3: idiomatic Go generator
│ │ ├── headerspec/ # Standalone header spec extraction tool
│ │ └── ndk-build/ # APK packaging tool
│ └── pkg/
│ ├── c2ffi/ # c2ffi invocation and JSON→spec conversion
│ ├── capigen/ # CGo code generation from spec + manifest
│ ├── headerspec/ # Header spec extraction logic
│ ├── specgen/ # Spec writing and legacy Go AST parsing
│ ├── specmodel/ # Spec data model (types, enums, functions)
│ ├── idiomgen/ # Merge, resolve, template rendering
│ └── overlaymodel/ # Overlay data model (annotations)
├── capi/
│ ├── manifests/ # capigen config per module (hand-written)
│ └── {module}/ # Generated raw CGo packages
├── spec/
│ ├── generated/ # Generated YAML specs (intermediate)
│ └── overlays/ # Hand-written semantic overlays
├── templates/ # Go text templates for code generation
├── {module}/ # Generated idiomatic Go packages (final output)
├── jni/ # Hand-written JNI helpers (activity, permissions, etc.)
├── e2e/ # End-to-end tests (Android emulator)
└── examples/ # Usage examples per module

Make Targets

TargetDescriptionRequires
make allRun all three stagesNDK + c2ffi
make specsStage 1: extract specs from C headersNDK + c2ffi
make capiStage 2: generate raw CGo bindingsspecs + manifests
make idiomaticStage 3: generate idiomatic packagesspecs + overlays
make regenClean and regenerate everythingNDK + c2ffi
make fixturesGenerate from testdata (no NDK needed)--
make testRun unit tests--
make e2eRun E2E tests on Android emulatorNDK + SDK + KVM
make apk-displaycameraBuild camera example APKNDK + SDK
make cleanRemove capi/*/ and spec/generated/--

Adding a New Module

  1. Create capi/manifests/{module}.yaml -- configure which NDK headers to parse and which symbols to accept.
  2. Run make specs to extract the structured spec from C headers via c2ffi.
  3. Run make capi to generate the raw CGo binding package.
  4. Create spec/overlays/{module}.yaml -- annotate types with Go names, assign functions as methods, configure constructors/destructors, error types, etc.
  5. Run make idiomatic to generate the final Go package.

Comparison with Similar Projects

Several other projects provide Go (or Rust) bindings for the Android NDK. The table below highlights how they differ in scope, API style, and maintenance status.

ndk (this project)gomobileandroid-gogooidrust-mobile/ndk
LanguageGoGoGoGoRust
NDK modules34 (graphics, camera, audio, media, sensors, ML, binder, …)~6 (app, gl, asset, sensor, audio, font — mostly in exp/)~7 (android core, EGL, GLES 1/2/3/3.1, NativeActivity)Core NDK + camera, sensor, audio via hand-written wrappers~18 (asset, audio, bitmap, config, font, hardware buffer, looper, media, native window, surface texture, trace, …)
API styleIdiomatic Go: methods on types, builders, Close(), error interface, chainingCross-platform abstraction; hides NDK behind portable APIsThin wrappers over C; raw NDK types with some hand-written helpersHand-written Go wrappers; Chinese documentationSafe Rust abstractions over raw FFI (ndk-sys)
Code generation3-stage pipeline (c2ffi → spec YAML → CGo → idiomatic Go)Hand-writtenc-for-go auto-generation from NDK headersManualbindgen for ndk-sys; hand-written safe layer
Target NDKCurrent (configurable via sysroot)Tied to gomobile toolchainandroid-23Unspecified (older)Current (configurable)
Camera2 NDKYesNoNoYes (partial, hand-written)No
AAudioYesNo (exp/audio uses OpenSL ES)NoYes (partial)Yes
MediaCodecYesNoNoNoYes (partial)
VulkanYesNoNoNoNo
NNAPIYesNoNoNoNo
Binder IPCYesNoNoNoNo
Callback bridgingAuto-generated C↔Go bridges with registryN/A (event loop abstraction)Manual via CGoManualRust closures / trait objects
Lifecycle mgmtIdempotent nil-safe Close() error on all typesManaged by frameworkManualManualDrop trait
Cross-platformAndroid onlyAndroid + iOSAndroid onlyAndroid onlyAndroid only
MaintenanceActive (2025)Active (official Go project)Inactive (last commit 2022)Inactive (last commit 2019)Active (2025)
Stars~6 100~1 100~60~1 300

Key Differences

  • gomobile is the official Go mobile toolkit. It prioritizes cross-platform portability (Android + iOS) over NDK coverage and exposes a small set of portable APIs (OpenGL ES, sensors, audio). It does not provide direct access to NDK-specific modules like Camera2, AAudio, MediaCodec, Vulkan, or Binder. If you need to call Android-specific NDK APIs, gomobile alone is not sufficient.

  • android-go (xlab) was a pioneering project that used c-for-go to auto-generate bindings from NDK headers. It covers EGL and OpenGL ES well but does not wrap higher-level NDK modules (camera, audio, media, sensors). The generated API exposes raw C types without idiomatic Go transformations. The project has not been updated since 2022.

  • gooid provides hand-written Go wrappers for a few NDK modules including camera and sensors, with example apps. Documentation is primarily in Chinese. The project has been inactive since 2019.

  • rust-mobile/ndk is the closest analog in the Rust ecosystem. It follows a similar two-layer approach (raw FFI in ndk-sys, safe abstractions in ndk) and covers ~18 NDK modules. It does not yet wrap Camera2, Vulkan, or NNAPI. If you are writing Rust rather than Go, this is the standard choice.

  • This project (AndroidGoLab/ndk) combines auto-generation with hand-written semantic overlays to produce idiomatic Go APIs across 34 NDK modules — the broadest coverage of any Go NDK binding library. The pipeline ensures that new NDK headers can be incorporated by updating manifests and overlays rather than rewriting bindings by hand.

Interoperability with related projects

All handle types expose UintPtr() uintptr and NewXFromUintPtr(uintptr) methods alongside the existing Pointer()/NewXFromPointer() pair. This enables interop with Go packages that represent native Android handles as uintptr — including golang.org/x/mobile, gioui.org, and github.com/xlab/android-go.

For generic code, all handles satisfy the handle.NativeHandle interface:

import"github.com/AndroidGoLab/ndk/handle"funclogHandle(h handle.NativeHandle) {
log.Printf("native handle: 0x%x", h.UintPtr())
}

EGL types (EGLDisplay, EGLContext, etc.) are unsafe.Pointer aliases and use free functions instead: egl.EGLDisplayToUintPtr(d) / egl.EGLDisplayFromUintPtr(p).

gomobile bind

gomobile bind does not support unsafe.Pointer or uintptr in exported APIs. Transport native handles as int64 (Java long) and convert inside Go:

package sensorbridge
import"github.com/AndroidGoLab/ndk/sensor"// Bridge wraps an ASensorManager for cross-language use.// Unexported fields are invisible to Java/Kotlin.typeBridgestruct {
mgr*sensor.Manager
}
// NewBridge creates a Bridge from a raw ASensorManager* passed as int64// (Java long). gomobile bind does not support unsafe.Pointer or uintptr,// so native handles must be transported as int64.funcNewBridge(managerHandleint64) *Bridge {
return&Bridge{
mgr: sensor.NewManagerFromUintPtr(uintptr(managerHandle)),
}
}
// Handle returns the underlying ASensorManager* as int64 for passing// back to Java/JNI code.func (b*Bridge) Handle() int64 {
returnint64(b.mgr.UintPtr())
}
// AccelerometerName returns the name of the default accelerometer,// or empty string if unavailable.func (b*Bridge) AccelerometerName() string {
s:=b.mgr.DefaultSensor(sensor.Accelerometer)
ifs.UintPtr() ==0 {
return""
}
returns.Name()
}
// SensorName returns the name of a sensor by its Android type code,// or empty string if the sensor is not available.func (b*Bridge) SensorName(sensorTypeint32) string {
s:=b.mgr.DefaultSensor(sensor.Type(sensorType))
ifs.UintPtr() ==0 {
return""
}
returns.Name()
}

Build AAR: gomobile bind -target=android -androidapi=35 ./examples/gomobile/sensorbridge

Java usage:

longptr = nativeGetSensorManager(); // from JNIBridgebridge = Sensorbridge.newBridge(ptr);
Stringname = bridge.accelerometerName();

See examples/gomobile/sensorbridge/ for the complete buildable example.

FAQ

Q: Why NativeActivity instead of GameActivity?

This project wraps NDK C headers via a three-stage generation pipeline. GameActivity is a Jetpack Java library (AAR in the AGDK), not an NDK C API. NativeActivity is the only activity model exposed by NDK headers themselves. GameActivity requires bundling a Java AAR + JNI bridging — fundamentally different from the C→Go pipeline. rust-mobile/ndk faces the identical constraint and also centers NativeActivity. A companion gameactivity/ package is planned for wrapping the AGDK GameActivity C library.

Q: What about permissions, text input, and other Java-only APIs?

Android runtime permissions are Activity-driven Java APIs; the NDK APermissionManager_checkPermission only checks, it cannot request. The jni/ package provides HasPermission() and RequestPermission() via JNI as the escape hatch. examples/camera/display/ demonstrates the full permission request flow. This is inherent to Android's platform design — all native-first frameworks (Rust ndk, C++ NativeActivity) hit the same boundary. See Platform Integration Guide for details.

Q: Why do examples use unsafe.Pointer and runtime.LockOSThread()?

unsafe.Pointer in audio I/O provides zero-copy buffer access — adding a copying wrapper would be a performance regression for the primary use case. runtime.LockOSThread() is the standard Go pattern for thread-affine APIs (EGL contexts, ALooper, AInputQueue) — it is not a leaky abstraction, it is how Go correctly interoperates with thread-local native APIs. The idiomatic layer hides unsafe.Pointer for all handle types behind typed Go structs with constructors and Close() methods. See Thread Safety Guide.

Q: Why make targets instead of Android Studio / Gradle?

There is no Go↔Gradle integration in the wider Go ecosystem; gomobile also uses a custom build tool. The provided Makefile produces complete, signed APKs ready for deployment. A standalone go-ndk-build CLI tool is planned to streamline APK packaging.

Q: Is the API stable?

The project is pre-release (v0.x). APIs may change as the overlay system evolves. Generated APIs track NDK header changes — running the pipeline with a new NDK version may change signatures. Semantic versioning will be adopted once the overlay format stabilizes.

Guides

About

Idiomatic Go bindings for 34 Android NDK modules — Camera2, AAudio, OpenGL ES, Vulkan, sensors, MediaCodec, and more.

Topics

Resources

Stars

7 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages