# Android Go NDK The Android Go NDK library provides idiomatic Go bindings for the Android NDK, auto-generated from C headers to ensure full coverage and easy maintenance. It wraps 34 NDK modules covering graphics/rendering (EGL, OpenGL ES, Vulkan), camera (Camera2 NDK), audio (AAudio), media codecs, sensors, neural networks (NNAPI), system services (Binder IPC), and more. The bindings feature type-safe APIs with proper resource lifecycle management through `Close()` methods, error handling via the `error` interface, and builder patterns for configuration. This project is part of a family of three Go libraries covering Android interface surfaces: **ndk** for NDK C API bindings via cgo, **jni** for Java API bindings via JNI+cgo, and **binder** for pure-Go Binder IPC access. The ndk library is ideal for high-performance hardware access including camera, audio, sensors, and OpenGL/Vulkan rendering where low-latency, low-overhead bindings directly to C libraries are essential. ## Audio Package - AAudio Stream Builder The `audio` package provides Go bindings for Android's AAudio API for low-latency audio playback and recording. The StreamBuilder type configures audio streams with chainable setter methods for sample rate, channel count, format, and performance mode. ```go package main import ( "log" "unsafe" "github.com/AndroidGoLab/ndk/audio" ) func main() { // Create audio stream builder builder, err := audio.NewStreamBuilder() if err != nil { log.Fatal(err) } defer builder.Close() // Configure stream with chainable methods builder. SetDirection(audio.Output). SetSampleRate(44100). SetChannelCount(2). SetFormat(audio.PcmFloat). SetPerformanceMode(audio.LowLatency). SetSharingMode(audio.Shared) // Open the configured stream 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()) // Start playback if err := stream.Start(); err != nil { log.Fatal(err) } defer stream.Stop() // Write audio data (silence in this example) buf := make([]float32, int(stream.FramesPerBurst())*2) stream.Write(unsafe.Pointer(&buf[0]), stream.FramesPerBurst(), 1_000_000_000) } ``` ## Audio Package - Microphone Recording The Stream type supports both playback and recording through direction configuration. Use `audio.Input` direction for microphone capture with PCM format audio data. ```go package main import ( "log" "math" "time" "unsafe" "github.com/AndroidGoLab/ndk/audio" ) func main() { builder, err := audio.NewStreamBuilder() if err != nil { log.Fatalf("create stream builder: %v", err) } defer builder.Close() // Configure for recording builder. SetDirection(audio.Input). SetSampleRate(48000). SetChannelCount(1). SetFormat(audio.PcmI16). SetPerformanceMode(audio.LowLatency). SetSharingMode(audio.Shared) stream, err := builder.Open() if err != nil { log.Fatalf("open stream: %v", err) } defer stream.Close() rate := stream.SampleRate() log.Printf("capture stream opened (rate=%d Hz, ch=%d)", rate, stream.ChannelCount()) if err := stream.Start(); err != nil { log.Fatalf("start stream: %v", err) } // Read approximately 1 second of audio totalFrames := rate buf := make([]int16, 1024) bufBytes := unsafe.Slice((*byte)(unsafe.Pointer(&buf[0])), len(buf)*int(unsafe.Sizeof(buf[0]))) var captured []int16 for int32(len(captured)) < totalFrames { framesToRead := int32(len(buf)) if remaining := totalFrames - int32(len(captured)); remaining < framesToRead { framesToRead = remaining } n, err := stream.Read(bufBytes, framesToRead, time.Second) if err != nil { log.Fatalf("read: %v", err) } captured = append(captured, buf[:n]...) } if err := stream.Stop(); err != nil { log.Fatalf("stop stream: %v", err) } // Compute peak amplitude var peak int16 for _, s := range captured { if s < 0 { s = -s } if s > 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)) } ``` ## Camera Package - Manager and Device Discovery The `camera` package wraps the Android Camera2 NDK API. The Manager type provides camera discovery and device access. All camera operations require the `android.permission.CAMERA` permission. ```go package main import ( "log" "github.com/AndroidGoLab/ndk/camera" ) func main() { // Create camera manager - entry point to Camera2 API mgr := camera.NewManager() defer mgr.Close() // List available cameras (requires CAMERA permission) ids, err := mgr.CameraIDList() if err != nil { log.Fatal(err) // camera.ErrPermissionDenied if CAMERA not granted } for _, id := range ids { // Get camera characteristics/metadata meta, err := mgr.GetCameraCharacteristics(id) if err != nil { log.Printf("camera %s: failed to get characteristics: %v", id, err) continue } // Query sensor orientation from metadata orientation := meta.I32At(uint32(camera.SensorOrientation), 0) log.Printf("camera %s: orientation=%d°", id, orientation) } } ``` ## Camera Package - Opening Camera and Capture Session Opening a camera device requires state callbacks for disconnect/error handling. Create capture requests and sessions to receive camera frames. ```go package main import ( "log" "github.com/AndroidGoLab/ndk/camera" ) func main() { mgr := camera.NewManager() defer mgr.Close() ids, err := mgr.CameraIDList() if err != nil || len(ids) == 0 { log.Fatal("no cameras available") } // Open camera with state callbacks device, err := mgr.OpenCamera(ids[0], camera.DeviceStateCallbacks{ OnDisconnected: func() { log.Println("camera disconnected") }, OnError: func(code int) { log.Printf("camera error: %d", code) }, }) if err != nil { log.Fatalf("open camera: %v", err) } defer device.Close() log.Printf("opened camera: %s", device.GetID()) // Create capture request for preview request, err := device.CreateCaptureRequest(camera.Preview) if err != nil { log.Fatalf("create capture request: %v", err) } defer request.Close() // In a real app, you would: // 1. Create OutputTarget from ANativeWindow // 2. Add target to request // 3. Create SessionOutputContainer // 4. Create CaptureSession // 5. Set repeating request for continuous preview } ``` ## Sensor Package - Sensor Manager and Queries The `sensor` package provides access to Android device sensors including accelerometer, gyroscope, light, proximity, and more. Use `GetInstance()` to obtain the singleton SensorManager. ```go package main import ( "fmt" "github.com/AndroidGoLab/ndk/sensor" ) func main() { // Get the sensor manager singleton mgr := sensor.GetInstance() // Query default accelerometer 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()) // Query other sensor types sensors := []struct { label string sensorType sensor.Type }{ {"Accelerometer", sensor.Accelerometer}, {"Gyroscope", sensor.Gyroscope}, {"Light", sensor.Light}, {"Proximity", sensor.Proximity}, {"Magnetic Field", sensor.MagneticField}, } fmt.Println("\nDefault sensors on this device:") for _, info := range sensors { s := mgr.DefaultSensor(info.sensorType) if s.UintPtr() == 0 { fmt.Printf(" %-16s not available\n", info.label+":") continue } fmt.Printf(" %s:\n", info.label) fmt.Printf(" Name: %s\n", s.Name()) fmt.Printf(" Vendor: %s\n", s.Vendor()) fmt.Printf(" Type: %s (%d)\n", info.sensorType, int32(info.sensorType)) fmt.Printf(" Resolution: %g\n", s.Resolution()) fmt.Printf(" Min delay: %d us\n", s.MinDelay()) } } ``` ## Thermal Package - Device Thermal Status The `thermal` package monitors device thermal state to adjust workload and prevent throttling. ThermalStatus ranges from None (cool) to Shutdown (critical). ```go package main import ( "fmt" "github.com/AndroidGoLab/ndk/thermal" ) func main() { mgr := thermal.NewManager() defer mgr.Close() status := mgr.CurrentStatus() fmt.Printf("Thermal status: %s (%d)\n", status, int32(status)) switch status { case thermal.StatusNone: fmt.Println("Device is cool.") case thermal.StatusLight, thermal.StatusModerate: fmt.Println("Device is warm; consider reducing workload.") case thermal.StatusSevere, thermal.StatusCritical: fmt.Println("Device is hot; throttling likely.") case thermal.StatusEmergency, thermal.StatusShutdown: fmt.Println("Device is critically hot; shutdown imminent.") default: fmt.Println("Unable to determine thermal status.") } // Get thermal headroom forecast headroom := mgr.AThermal_getThermalHeadroom(10) // 10 second forecast fmt.Printf("Thermal headroom (10s forecast): %.2f\n", headroom) } ``` ## EGL Package - Display Initialization and Context Creation The `egl` package wraps the EGL API for creating OpenGL ES rendering contexts. Initialize a display, choose a config, and create surfaces and contexts for rendering. ```go package main import ( "fmt" "log" "github.com/AndroidGoLab/ndk/egl" ) func main() { // Get the default EGL display dpy := egl.GetDisplay(egl.EGLNativeDisplayType(0)) if dpy == nil { log.Fatal("eglGetDisplay failed") } // Initialize EGL var major, minor egl.Int if egl.Initialize(dpy, &major, &minor) == egl.False { log.Fatalf("eglInitialize failed: 0x%x", egl.GetError()) } defer egl.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)) // Choose a config with ES2 support attribs := []egl.Int{ egl.RenderableType, egl.OpenglEs2Bit, egl.SurfaceType, egl.PbufferBit, egl.RedSize, 8, egl.GreenSize, 8, egl.BlueSize, 8, egl.None, } var cfg egl.EGLConfig var numCfg egl.Int if egl.ChooseConfig(dpy, &attribs[0], &cfg, 1, &numCfg) == egl.False || numCfg == 0 { log.Fatal("eglChooseConfig failed") } // Create pbuffer surface and 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]) if ctx == nil { log.Fatal("eglCreateContext failed") } defer egl.DestroyContext(dpy, ctx) defer egl.DestroySurface(dpy, surface) // Make context current egl.MakeCurrent(dpy, surface, surface, ctx) fmt.Println("EGL context created successfully") egl.MakeCurrent(dpy, nil, nil, nil) } ``` ## GLES2 Package - OpenGL ES 2.0 Rendering The `gles2` package provides OpenGL ES 2.0 bindings for GPU-accelerated rendering. Requires an active EGL context. ```go package main import ( "log" "unsafe" "github.com/AndroidGoLab/ndk/egl" "github.com/AndroidGoLab/ndk/gles2" ) func goString(p *gles2.GLubyte) string { if p == nil { return "" } var buf []byte for ptr := (*byte)(unsafe.Pointer(p)); *ptr != 0; ptr = (*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(ptr)) + 1)) { buf = append(buf, *ptr) } return string(buf) } func main() { // Setup EGL context (abbreviated - see EGL example for full setup) dpy := egl.GetDisplay(egl.EGLNativeDisplayType(0)) var major, minor egl.Int egl.Initialize(dpy, &major, &minor) defer egl.Terminate(dpy) attribs := []egl.Int{egl.RenderableType, egl.OpenglEs2Bit, egl.SurfaceType, egl.PbufferBit, egl.None} var cfg egl.EGLConfig var numCfg egl.Int egl.ChooseConfig(dpy, &attribs[0], &cfg, 1, &numCfg) pbuf := []egl.Int{egl.Width, 256, egl.Height, 256, egl.None} surface := egl.CreatePbufferSurface(dpy, cfg, &pbuf[0]) ctxAttribs := []egl.Int{egl.ContextClientVersion, 2, egl.None} ctx := egl.CreateContext(dpy, cfg, nil, &ctxAttribs[0]) defer egl.DestroyContext(dpy, ctx) defer egl.DestroySurface(dpy, surface) egl.MakeCurrent(dpy, surface, surface, ctx) defer egl.MakeCurrent(dpy, nil, nil, nil) // Query OpenGL ES info log.Printf("GL Vendor: %s", goString(gles2.GetString(gles2.GL_VENDOR))) log.Printf("GL Renderer: %s", goString(gles2.GetString(gles2.GL_RENDERER))) log.Printf("GL Version: %s", goString(gles2.GetString(gles2.GL_VERSION))) // Clear screen to red gles2.ClearColor(1.0, 0.0, 0.0, 1.0) gles2.Clear(gles2.GL_COLOR_BUFFER_BIT) // Set viewport gles2.Viewport(0, 0, 256, 256) // Create and compile shader shader := gles2.CreateShader(gles2.GL_VERTEX_SHADER) // ... shader source and compilation gles2.DeleteShader(shader) log.Println("OpenGL ES 2.0 rendering complete") } ``` ## Config Package - Device Configuration The `config` package queries Android device configuration including screen density, orientation, dimensions, and SDK version. ```go package main import ( "fmt" "github.com/AndroidGoLab/ndk/config" ) func main() { cfg := config.NewConfig() defer cfg.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()) switch config.Orientation(cfg.Orientation()) { case config.OrientationPort: fmt.Println(" (portrait)") case config.OrientationLand: fmt.Println(" (landscape)") case config.OrientationSquare: fmt.Println(" (square)") default: fmt.Println(" (any/unknown)") } } ``` ## Media Package - Codec Creation and Configuration The `media` package wraps Android's MediaCodec API for hardware-accelerated video and audio encoding/decoding. ```go package main import ( "fmt" "github.com/AndroidGoLab/ndk/media" ) func main() { codecs := []struct { mime string desc string }{ {"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 := range codecs { encOK := "no" enc := media.NewEncoder(c.mime) if enc != nil && enc.Pointer() != nil { encOK = "yes" enc.Close() } decOK := "no" dec := media.NewDecoder(c.mime) if dec != nil && dec.Pointer() != nil { decOK = "yes" dec.Close() } fmt.Printf("%-28s %-10s %-10s (%s)\n", c.mime, encOK, decOK, c.desc) } } ``` ## Looper Package - Event Loop Management The `looper` package provides Go bindings for Android's ALooper event loop, used for sensor event queues, input handling, and other async operations. ```go package main import ( "log" "runtime" "time" "unsafe" "github.com/AndroidGoLab/ndk/looper" ) func main() { // Lock to OS thread - required for thread-affine APIs runtime.LockOSThread() defer runtime.UnlockOSThread() // Prepare looper for current thread lp := looper.Prepare(int32(looper.ALOOPER_PREPARE_ALLOW_NON_CALLBACKS)) defer lp.Close() // Acquire reference to keep looper alive lp.Acquire() // Wake the looper from another goroutine go func() { time.Sleep(100 * time.Millisecond) lp.Wake() }() // Poll for events 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 from Wake() call") case looper.ALOOPER_POLL_TIMEOUT: log.Println("timed out") case looper.ALOOPER_POLL_ERROR: log.Println("poll error") default: log.Printf("poll result: %d", result) } } ``` ## Asset Package - Loading Bundled Resources The `asset` package provides access to files bundled in the APK's assets directory through the AssetManager. ```go package main import ( "fmt" "io" "unsafe" "github.com/AndroidGoLab/ndk/asset" ) func main() { // In a real NativeActivity app, obtain manager from activity: // mgr := asset.NewManagerFromPointer(activity.AssetManager(nativeActivity)) var mgr *asset.Manager // = activity.AssetManager(nativeActivity) // Open an asset file in streaming mode a := mgr.Open("textures/wood.png", asset.Streaming) defer a.Close() // Get asset size and read contents size := a.Length() buf := make([]byte, size) _, _ = io.ReadFull(unsafe.NewReader(a), buf) fmt.Printf("read %d bytes from asset\n", len(buf)) // Open asset directory dir := mgr.OpenDir("textures") defer dir.Close() // Iterate directory contents for { name := dir.GetNextFileName() if name == "" { break } fmt.Printf(" asset: %s\n", name) } } ``` ## Image Package - Image Decoding The `image` package decodes JPEG, PNG, and other image formats using Android's AImageDecoder API. ```go package main import ( "fmt" "log" "syscall" "unsafe" "github.com/AndroidGoLab/ndk/image" ) func main() { // Open image file via POSIX fd fd, err := syscall.Open("/sdcard/photo.jpg", syscall.O_RDONLY, 0) if err != nil { log.Fatalf("open file: %v", err) } defer syscall.Close(fd) // Create decoder from file descriptor decoder, err := image.NewDecoderFromFd(int32(fd)) if err != nil { log.Fatalf("create decoder: %v", err) } defer decoder.Close() // Get image dimensions from header header := decoder.HeaderInfo() // width and height available from header info // Get stride and allocate pixel buffer stride := decoder.MinimumStride() height := int32(480) // example height bufSize := stride * uint64(height) pixels := make([]byte, bufSize) fmt.Printf("Stride: %d bytes, buffer: %d bytes\n", stride, bufSize) // Decode image into buffer if err := decoder.Decode(unsafe.Pointer(&pixels[0]), stride, bufSize); err != nil { log.Fatalf("decode: %v", err) } // Check if animated (GIF, WebP animation) if decoder.IsAnimated() { fmt.Println("Image is animated") // Use AdvanceFrame() to decode subsequent frames } fmt.Printf("Decoded %d bytes of pixel data\n", bufSize) } ``` ## Handle Interoperability All handle types expose `UintPtr()` and `NewXFromUintPtr()` methods for interoperability with other Go packages that use `uintptr` for native handles, including `golang.org/x/mobile`, `gioui.org`, and `github.com/xlab/android-go`. ```go package main import ( "log" "github.com/AndroidGoLab/ndk/handle" "github.com/AndroidGoLab/ndk/sensor" ) // All handles implement the NativeHandle interface func logHandle(h handle.NativeHandle) { log.Printf("native handle: 0x%x", h.UintPtr()) } func main() { mgr := sensor.GetInstance() // Get handle as uintptr for interop ptr := mgr.UintPtr() log.Printf("manager uintptr: 0x%x", ptr) // Reconstruct from uintptr mgr2 := sensor.NewManagerFromUintPtr(ptr) // Use with generic interface logHandle(mgr2) // For gomobile bind (which doesn't support uintptr): // Transport as int64 and convert: int64(mgr.UintPtr()) } ``` The Android Go NDK library is designed for Go developers building native Android applications requiring direct access to device hardware and platform services. Primary use cases include real-time audio processing applications using AAudio, camera applications with Camera2 NDK for preview and capture pipelines, sensor-driven applications reading accelerometer/gyroscope/proximity data, and GPU-accelerated rendering with OpenGL ES or Vulkan. The library supports performance-critical applications where the overhead of Java/JNI calls would be prohibitive. Integration follows standard Go patterns: import the required packages, create manager/builder types, configure with chainable methods, and ensure proper resource cleanup with `defer Close()`. All types implement idempotent, nil-safe Close() methods. Error types wrap NDK status codes and work with `errors.Is()` for error checking. For thread-affine APIs like EGL contexts and ALooper, use `runtime.LockOSThread()`. The library can be combined with the companion `jni` package for Java-only APIs (Bluetooth, WiFi, NFC) and the `binder` package for pure-Go system service access without cgo dependencies.