Skip to content

Commit 02b59e1

Browse files
xaionaro@dx.centerxaionaro@dx.center
authored andcommitted
No comment
1 parent c5ee54d commit 02b59e1

73 files changed

Lines changed: 2786 additions & 181 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Makefile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ FIXTURE_MODULES := $(notdir $(wildcard tools/pkg/specgen/testdata/*/))
1717
NDK_SYSROOT := $(NDK_PATH)/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include
1818
C2FFI_BIN ?= c2ffi
1919

20-
.PHONY: all capi specs idiomatic clean regen fixtures test lint check-examples e2e e2e-build e2e-examples
20+
.PHONY: all capi specs idiomatic clean regen fixtures test lint check-examples e2e e2e-build e2e-examples e2e-audio
2121

2222
all: specs capi idiomatic
2323

@@ -108,6 +108,10 @@ e2e: e2e-build
108108
e2e-examples:
109109
./tests/e2e/run-examples.sh
110110

111+
# Run audio recording E2E test (requires running emulator with audio + NDK)
112+
e2e-audio:
113+
./tests/e2e/run-audio-e2e.sh
114+
111115
clean:
112116
@for m in $(MODULES); do rm -rf "capi/$$m/"; done
113117
rm -rf spec/generated/

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77

88
Idiomatic Go bindings for the Android NDK, auto-generated from C headers to ensure full coverage and easy maintenance.
99

10+
## Requirements
11+
12+
- **Android NDK r28** (28.0.13004108) or later
13+
- **API level 35** (Android 15) target
14+
1015
## Usage Examples
1116

1217
### Audio Playback (AAudio)
@@ -602,3 +607,30 @@ bindings, proper resource lifecycle (Close), error handling, and callback
602607
bridging. Writing CGo from scratch duplicates this work and lacks the
603608
automated test coverage this project maintains.
604609
-->
610+
611+
## FAQ
612+
613+
**Q: Why NativeActivity instead of GameActivity?**
614+
615+
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.
616+
617+
**Q: What about permissions, text input, and other Java-only APIs?**
618+
619+
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](docs/platform-integration.md) for details.
620+
621+
**Q: Why do examples use `unsafe.Pointer` and `runtime.LockOSThread()`?**
622+
623+
`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](docs/thread-safety.md).
624+
625+
**Q: Why `make` targets instead of Android Studio / Gradle?**
626+
627+
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.
628+
629+
**Q: Is the API stable?**
630+
631+
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.
632+
633+
## Guides
634+
635+
- [Thread Safety](docs/thread-safety.md) — when and why to use `runtime.LockOSThread()`
636+
- [Platform Integration](docs/platform-integration.md) — bridging to Java APIs via JNI

android/app.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
//go:build android
2+
3+
package android
4+
5+
import (
6+
"sync"
7+
"unsafe"
8+
9+
"github.com/xaionaro-go/ndk/activity"
10+
"github.com/xaionaro-go/ndk/window"
11+
)
12+
13+
// App provides a high-level interface to an Android NativeActivity.
14+
type App struct {
15+
mu sync.Mutex
16+
activity *activity.Activity
17+
window *window.Window
18+
windowPtr unsafe.Pointer
19+
}
20+
21+
// Activity returns the underlying Activity.
22+
func (app *App) Activity() *activity.Activity {
23+
app.mu.Lock()
24+
defer app.mu.Unlock()
25+
return app.activity
26+
}
27+
28+
// Window returns the current native window, or nil if no window is available.
29+
func (app *App) Window() *window.Window {
30+
app.mu.Lock()
31+
defer app.mu.Unlock()
32+
return app.window
33+
}
34+
35+
// WindowSize returns the current window dimensions.
36+
// Returns (0, 0) if no window is available.
37+
func (app *App) WindowSize() (width, height int32) {
38+
app.mu.Lock()
39+
w := app.window
40+
app.mu.Unlock()
41+
42+
if w == nil {
43+
return 0, 0
44+
}
45+
return w.Width(), w.Height()
46+
}
47+
48+
func (app *App) setActivity(act *activity.Activity) {
49+
app.mu.Lock()
50+
defer app.mu.Unlock()
51+
app.activity = act
52+
}
53+
54+
func (app *App) setWindow(ptr unsafe.Pointer) {
55+
app.mu.Lock()
56+
defer app.mu.Unlock()
57+
if ptr == nil {
58+
app.window = nil
59+
app.windowPtr = nil
60+
} else {
61+
app.windowPtr = ptr
62+
app.window = window.NewWindowFromPointer(ptr)
63+
}
64+
}

android/doc.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
//go:build android
2+
3+
// Package android provides a high-level API for Android NativeActivity apps.
4+
//
5+
// It combines the lower-level NDK packages (activity, window, input) with
6+
// JNI helpers (jni) into an ergonomic interface for common platform tasks
7+
// like permission management, lifecycle handling, and UI operations.
8+
package android

android/lifecycle.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
//go:build android
2+
3+
package android
4+
5+
import (
6+
"unsafe"
7+
8+
"github.com/xaionaro-go/ndk/activity"
9+
"github.com/xaionaro-go/ndk/input"
10+
)
11+
12+
// Handlers defines callbacks for Android lifecycle events and input.
13+
// All callbacks receive the App instance for accessing platform APIs.
14+
type Handlers struct {
15+
OnCreate func(app *App)
16+
OnResume func(app *App)
17+
OnPause func(app *App)
18+
OnDestroy func(app *App)
19+
OnWindowCreated func(app *App)
20+
OnWindowDestroyed func(app *App)
21+
OnWindowFocusChanged func(app *App, hasFocus bool)
22+
OnInputEvent func(app *App, event *input.Event) bool
23+
}
24+
25+
// Run sets up NativeActivity lifecycle callbacks and manages the App instance.
26+
// Call this from an init() function in your main package.
27+
func Run(handlers Handlers) {
28+
app := &App{}
29+
30+
activity.SetLifecycleCallbacks(activity.LifecycleCallbacks{
31+
OnCreate: func(act *activity.Activity) {
32+
app.setActivity(act)
33+
if handlers.OnCreate != nil {
34+
handlers.OnCreate(app)
35+
}
36+
},
37+
OnNativeWindowCreated: func(act *activity.Activity, win unsafe.Pointer) {
38+
app.setActivity(act)
39+
app.setWindow(win)
40+
if handlers.OnWindowCreated != nil {
41+
handlers.OnWindowCreated(app)
42+
}
43+
},
44+
OnResume: func(act *activity.Activity) {
45+
app.setActivity(act)
46+
if handlers.OnResume != nil {
47+
handlers.OnResume(app)
48+
}
49+
},
50+
OnPause: func(act *activity.Activity) {
51+
if handlers.OnPause != nil {
52+
handlers.OnPause(app)
53+
}
54+
},
55+
OnWindowFocusChanged: func(_ *activity.Activity, hasFocus int32) {
56+
if handlers.OnWindowFocusChanged != nil {
57+
handlers.OnWindowFocusChanged(app, hasFocus != 0)
58+
}
59+
},
60+
OnNativeWindowDestroyed: func(_ *activity.Activity, _ unsafe.Pointer) {
61+
if handlers.OnWindowDestroyed != nil {
62+
handlers.OnWindowDestroyed(app)
63+
}
64+
app.setWindow(nil)
65+
},
66+
OnInputQueueCreated: func(_ *activity.Activity, queuePtr unsafe.Pointer) {
67+
if handlers.OnInputEvent != nil {
68+
q := input.NewQueueFromPointer(queuePtr)
69+
go drainInput(app, q, handlers.OnInputEvent)
70+
}
71+
},
72+
OnDestroy: func(_ *activity.Activity) {
73+
if handlers.OnDestroy != nil {
74+
handlers.OnDestroy(app)
75+
}
76+
app.setActivity(nil)
77+
app.setWindow(nil)
78+
},
79+
})
80+
}
81+
82+
func drainInput(
83+
app *App,
84+
q *input.Queue,
85+
handler func(app *App, event *input.Event) bool,
86+
) {
87+
for {
88+
ev := q.GetEvent()
89+
if ev == nil {
90+
return
91+
}
92+
93+
if q.PreDispatchEvent(ev) {
94+
continue
95+
}
96+
97+
handled := handler(app, ev)
98+
var result int32
99+
if handled {
100+
result = 1
101+
}
102+
q.FinishEvent(ev, result)
103+
}
104+
}

android/permissions.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//go:build android
2+
3+
package android
4+
5+
import (
6+
"github.com/xaionaro-go/ndk/jni"
7+
)
8+
9+
// HasPermission checks if a runtime permission is currently granted.
10+
func (app *App) HasPermission(permission string) bool {
11+
app.mu.Lock()
12+
act := app.activity
13+
app.mu.Unlock()
14+
15+
if act == nil {
16+
return false
17+
}
18+
return jni.HasPermission(act.Pointer(), permission)
19+
}
20+
21+
// RequestPermission shows the system permission dialog for the given permission.
22+
// This is asynchronous; use HasPermission to check the result after the user responds.
23+
func (app *App) RequestPermission(permission string) {
24+
app.mu.Lock()
25+
act := app.activity
26+
app.mu.Unlock()
27+
28+
if act == nil {
29+
return
30+
}
31+
jni.RequestPermission(act.Pointer(), permission)
32+
}

android/ui.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//go:build android
2+
3+
package android
4+
5+
import (
6+
"github.com/xaionaro-go/ndk/jni"
7+
)
8+
9+
// ShowToast displays an Android Toast message.
10+
func (app *App) ShowToast(message string) {
11+
app.mu.Lock()
12+
act := app.activity
13+
app.mu.Unlock()
14+
15+
if act == nil {
16+
return
17+
}
18+
jni.ShowToast(act.Pointer(), message)
19+
}
20+
21+
// FillWindowColor fills the current window with a solid RGBA color.
22+
// Does nothing if no window is available.
23+
func (app *App) FillWindowColor(color uint32) {
24+
app.mu.Lock()
25+
wPtr := app.windowPtr
26+
app.mu.Unlock()
27+
28+
if wPtr == nil {
29+
return
30+
}
31+
jni.FillWindowColor(wPtr, color)
32+
}

audio/stream.go

Lines changed: 12 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

audio/stream_builder.go

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)