initial commit

This commit is contained in:
2026-09-03 10:44:30 +02:00
commit 891fadca85
18 changed files with 2518 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
*.apk
*.apk.*
*keystore
android/lib/lib/arm64-v8a/libmain.so
+39
View File
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.illusionman1212.dogfood2"
android:versionCode="1"
android:versionName="1.0">
<uses-sdk android:minSdkVersion="24" android:targetSdkVersion="37" />
<uses-feature android:glEsVersion="0x00030002" android:required="true" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<application
android:label="Dogfooding"
android:hasCode="false"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:debuggable="false">
<activity android:name="android.app.NativeActivity"
android:configChanges="orientation|keyboardHidden"
android:exported="true"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen">
<meta-data android:name="android.app.lib_name"
android:value="main" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
@@ -0,0 +1,22 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="260dp"
android:height="260dp"
android:viewportWidth="260"
android:viewportHeight="260">
<path
android:pathData="m260.84,130.84c0,71.8 -58.2,130 -130,130 -71.8,0 -130,-58.2 -130,-130 0,-71.8 58.2,-130 130,-130 71.8,0 130,58.2 130,130z">
<aapt:attr name="android:fillColor">
<gradient
android:centerX="29.32"
android:centerY="29.32"
android:gradientRadius="203.03"
android:type="radial">
<item android:offset="0" android:color="#FF189CFF"/>
<item android:offset="0.58" android:color="#FF138EEB"/>
<item android:offset="0.8" android:color="#FF0971BF"/>
<item android:offset="1" android:color="#FF005493"/>
</gradient>
</aapt:attr>
</path>
</vector>
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="260dp"
android:height="260dp"
android:viewportWidth="260"
android:viewportHeight="260">
<path
android:pathData="m101.51,171.2c-1.92,-1.32 -3.44,-2.6 -3.44,-2.6L147.46,83.06c0,0 2.03,0.75 3.71,1.56 2.12,1.01 3.87,2 3.87,2 23.94,13.82 32.16,44.48 18.33,68.42 -13.82,23.94 -44.48,32.16 -68.42,18.33 0,0 -1.68,-0.96 -3.45,-2.18zM150.87,93.85 L109.13,166.15c19.95,11.52 45.5,4.67 57.02,-15.28 11.52,-19.95 4.67,-45.5 -15.28,-57.02zM88.9,158.64c-1.68,-2.43 -2.28,-3.6 -2.28,-3.6 -8.63,-14.99 -9.26,-34.05 0,-50.09 9.26,-16.04 26.08,-25.02 43.38,-25.04 0,0 2.33,-0.01 4.28,0.18 2.93,0.28 4.88,0.66 4.88,0.66L91.92,162.56c0,0 -1.12,-1.16 -3.03,-3.92zM125,88.57c-12.59,1.53 -24.33,8.75 -31.15,20.56 -6.82,11.82 -7.2,25.59 -2.23,37.26z"
android:fillColor="#ffffff"/>
</vector>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Executable
+32
View File
@@ -0,0 +1,32 @@
#!/bin/bash
# Create a debug signing key to sign our application.
if [ ! -f .keystore ]; then
keytool -genkey -dname "CN=Android Debug, O=Android, C=US" -keystore .keystore -alias androiddebugkey -storepass android -keypass android -keyalg RSA -validity 30000
if [ $? -ne 0 ]; then exit 1; fi
fi
# Build the shared lib
if [[ $1 == "release" ]]; then
odin build . -target:linux_arm64 -subtarget:android -build-mode:shared -o:speed -define:RELEASE_BUILD=true
else
odin build . -target:linux_arm64 -subtarget:android -build-mode:shared -debug
fi
name=$(basename "$PWD")
# Create the proper lib directories and move the shared lib to them
# Rename the shared lib to match what we specified in the AndroidManifest.xml file
# for the android.app.lib_name metadata value
mkdir -p android/lib/lib/arm64-v8a
mv $name.so android/lib/lib/arm64-v8a/libmain.so || exit 1
# Bundle the directory into an apk
odin bundle android android -android-keystore:.keystore -android-keystore-password:"android"
cleanup() {
rm test.apk-build
}
cleanup
+256
View File
@@ -0,0 +1,256 @@
// Array marshalling between Odin slices and Java arrays.
//
// GENERATED by jnigen. Do not edit.
//
// Every to_* returns a LOCAL ref the caller must free; every from_* allocates
// with the given allocator and the caller owns the result.
package jni
import "shared:android"
to_bool_array :: proc(e: Env, vals: []bool) -> android.jbooleanArray {
arr := e^^.NewBooleanArray(e, android.jsize(len(vals)))
if arr == nil || len(vals) == 0 {
return arr
}
// Identical layout, so the slice can be handed over without a copy.
e^^.SetBooleanArrayRegion(e, arr, 0, android.jsize(len(vals)), cast([^]android.jboolean)raw_data(vals))
return arr
}
from_bool_array :: proc(e: Env, arr: android.jbooleanArray, allocator := context.allocator) -> []bool {
if arr == nil {
return nil
}
n := int(e^^.GetArrayLength(e, android.jarray(arr)))
if n == 0 {
return nil
}
out := make([]bool, n, allocator)
e^^.GetBooleanArrayRegion(e, arr, 0, android.jsize(n), cast([^]android.jboolean)raw_data(out))
return out
}
to_i8_array :: proc(e: Env, vals: []i8) -> android.jbyteArray {
arr := e^^.NewByteArray(e, android.jsize(len(vals)))
if arr == nil || len(vals) == 0 {
return arr
}
// Identical layout, so the slice can be handed over without a copy.
e^^.SetByteArrayRegion(e, arr, 0, android.jsize(len(vals)), cast([^]android.jbyte)raw_data(vals))
return arr
}
from_i8_array :: proc(e: Env, arr: android.jbyteArray, allocator := context.allocator) -> []i8 {
if arr == nil {
return nil
}
n := int(e^^.GetArrayLength(e, android.jarray(arr)))
if n == 0 {
return nil
}
out := make([]i8, n, allocator)
e^^.GetByteArrayRegion(e, arr, 0, android.jsize(n), cast([^]android.jbyte)raw_data(out))
return out
}
to_u16_array :: proc(e: Env, vals: []u16) -> android.jcharArray {
arr := e^^.NewCharArray(e, android.jsize(len(vals)))
if arr == nil || len(vals) == 0 {
return arr
}
// Identical layout, so the slice can be handed over without a copy.
e^^.SetCharArrayRegion(e, arr, 0, android.jsize(len(vals)), cast([^]android.jchar)raw_data(vals))
return arr
}
from_u16_array :: proc(e: Env, arr: android.jcharArray, allocator := context.allocator) -> []u16 {
if arr == nil {
return nil
}
n := int(e^^.GetArrayLength(e, android.jarray(arr)))
if n == 0 {
return nil
}
out := make([]u16, n, allocator)
e^^.GetCharArrayRegion(e, arr, 0, android.jsize(n), cast([^]android.jchar)raw_data(out))
return out
}
to_i16_array :: proc(e: Env, vals: []i16) -> android.jshortArray {
arr := e^^.NewShortArray(e, android.jsize(len(vals)))
if arr == nil || len(vals) == 0 {
return arr
}
// Identical layout, so the slice can be handed over without a copy.
e^^.SetShortArrayRegion(e, arr, 0, android.jsize(len(vals)), cast([^]android.jshort)raw_data(vals))
return arr
}
from_i16_array :: proc(e: Env, arr: android.jshortArray, allocator := context.allocator) -> []i16 {
if arr == nil {
return nil
}
n := int(e^^.GetArrayLength(e, android.jarray(arr)))
if n == 0 {
return nil
}
out := make([]i16, n, allocator)
e^^.GetShortArrayRegion(e, arr, 0, android.jsize(n), cast([^]android.jshort)raw_data(out))
return out
}
to_i32_array :: proc(e: Env, vals: []i32) -> android.jintArray {
arr := e^^.NewIntArray(e, android.jsize(len(vals)))
if arr == nil || len(vals) == 0 {
return arr
}
// Identical layout, so the slice can be handed over without a copy.
e^^.SetIntArrayRegion(e, arr, 0, android.jsize(len(vals)), cast([^]android.jint)raw_data(vals))
return arr
}
from_i32_array :: proc(e: Env, arr: android.jintArray, allocator := context.allocator) -> []i32 {
if arr == nil {
return nil
}
n := int(e^^.GetArrayLength(e, android.jarray(arr)))
if n == 0 {
return nil
}
out := make([]i32, n, allocator)
e^^.GetIntArrayRegion(e, arr, 0, android.jsize(n), cast([^]android.jint)raw_data(out))
return out
}
to_i64_array :: proc(e: Env, vals: []i64) -> android.jlongArray {
arr := e^^.NewLongArray(e, android.jsize(len(vals)))
if arr == nil || len(vals) == 0 {
return arr
}
// Identical layout, so the slice can be handed over without a copy.
e^^.SetLongArrayRegion(e, arr, 0, android.jsize(len(vals)), cast([^]android.jlong)raw_data(vals))
return arr
}
from_i64_array :: proc(e: Env, arr: android.jlongArray, allocator := context.allocator) -> []i64 {
if arr == nil {
return nil
}
n := int(e^^.GetArrayLength(e, android.jarray(arr)))
if n == 0 {
return nil
}
out := make([]i64, n, allocator)
e^^.GetLongArrayRegion(e, arr, 0, android.jsize(n), cast([^]android.jlong)raw_data(out))
return out
}
to_f32_array :: proc(e: Env, vals: []f32) -> android.jfloatArray {
arr := e^^.NewFloatArray(e, android.jsize(len(vals)))
if arr == nil || len(vals) == 0 {
return arr
}
// Identical layout, so the slice can be handed over without a copy.
e^^.SetFloatArrayRegion(e, arr, 0, android.jsize(len(vals)), cast([^]android.jfloat)raw_data(vals))
return arr
}
from_f32_array :: proc(e: Env, arr: android.jfloatArray, allocator := context.allocator) -> []f32 {
if arr == nil {
return nil
}
n := int(e^^.GetArrayLength(e, android.jarray(arr)))
if n == 0 {
return nil
}
out := make([]f32, n, allocator)
e^^.GetFloatArrayRegion(e, arr, 0, android.jsize(n), cast([^]android.jfloat)raw_data(out))
return out
}
to_f64_array :: proc(e: Env, vals: []f64) -> android.jdoubleArray {
arr := e^^.NewDoubleArray(e, android.jsize(len(vals)))
if arr == nil || len(vals) == 0 {
return arr
}
// Identical layout, so the slice can be handed over without a copy.
e^^.SetDoubleArrayRegion(e, arr, 0, android.jsize(len(vals)), cast([^]android.jdouble)raw_data(vals))
return arr
}
from_f64_array :: proc(e: Env, arr: android.jdoubleArray, allocator := context.allocator) -> []f64 {
if arr == nil {
return nil
}
n := int(e^^.GetArrayLength(e, android.jarray(arr)))
if n == 0 {
return nil
}
out := make([]f64, n, allocator)
e^^.GetDoubleArrayRegion(e, arr, 0, android.jsize(n), cast([^]android.jdouble)raw_data(out))
return out
}
// Object arrays. `elem_class` is the array's component type.
to_object_array :: proc(e: Env, vals: []android.jobject, elem_class: android.jclass) -> android.jobjectArray {
arr := e^^.NewObjectArray(e, android.jsize(len(vals)), elem_class, nil)
if arr == nil {
return nil
}
for v, i in vals {
e^^.SetObjectArrayElement(e, arr, android.jsize(i), v)
}
return arr
}
from_object_array :: proc(e: Env, arr: android.jobjectArray, allocator := context.allocator) -> []android.jobject {
if arr == nil {
return nil
}
n := int(e^^.GetArrayLength(e, android.jarray(arr)))
if n == 0 {
return nil
}
out := make([]android.jobject, n, allocator)
for i in 0 ..< n {
out[i] = e^^.GetObjectArrayElement(e, arr, android.jsize(i))
}
return out
}
// String arrays need per-element conversion in both directions.
to_string_array :: proc(e: Env, vals: []string) -> android.jobjectArray {
cls := class_ref(e, "java/lang/String")
if cls == nil {
return nil
}
arr := e^^.NewObjectArray(e, android.jsize(len(vals)), cls, nil)
if arr == nil {
return nil
}
for v, i in vals {
s := to_jstring(e, v)
e^^.SetObjectArrayElement(e, arr, android.jsize(i), android.jobject(s))
free_local(e, android.jobject(s))
}
return arr
}
from_string_array :: proc(e: Env, arr: android.jobjectArray, allocator := context.allocator) -> []string {
if arr == nil {
return nil
}
n := int(e^^.GetArrayLength(e, android.jarray(arr)))
if n == 0 {
return nil
}
out := make([]string, n, allocator)
for i in 0 ..< n {
el := e^^.GetObjectArrayElement(e, arr, android.jsize(i))
out[i] = from_jstring(e, android.jstring(el), allocator)
free_local(e, el)
}
return out
}
+178
View File
@@ -0,0 +1,178 @@
// Runtime support for generated JNI bindings: VM/env acquisition, exception
// handling, cached class lookup, and string marshalling.
//
package jni
import "core:strings"
import "shared:android"
Env :: ^android.JNIEnv
@(private)
vm: ^android.JavaVM
// Records the process JavaVM. Call once from android_main before any binding.
init :: proc(app: ^android.android_app) -> bool {
if app == nil || app.activity == nil {
return false
}
vm = app.activity.vm
return vm != nil
}
// The Activity instance, usable anywhere a Context is expected.
activity :: proc(app: ^android.android_app) -> android.jobject {
if app == nil || app.activity == nil {
return nil
}
return app.activity.clazz
}
// The engine's loop runs on a glue-spawned thread, not the one holding
// activity.env, so the env has to come from the VM and the thread attached on
// first use. The thread lives as long as the app, so it is never detached.
@(thread_local)
tls_env: Env
env :: proc() -> Env {
if tls_env != nil {
return tls_env
}
if vm == nil {
return nil
}
e: Env
if vm^^.GetEnv(vm, cast(^rawptr)&e, android.JNI_VERSION_1_6) == android.JNI_EDETACHED {
if vm^^.AttachCurrentThread(vm, &e, nil) != android.JNI_OK {
return nil
}
}
tls_env = e
return e
}
//
// Exceptions
//
// Reports whether a Java exception is pending, printing and clearing it if so.
// Left pending, an exception makes every later JNI call on this thread fail.
failed :: proc(e: Env) -> bool {
if e == nil {
return true
}
if e^^.ExceptionCheck(e) == 0 {
return false
}
e^^.ExceptionDescribe(e)
e^^.ExceptionClear(e)
return true
}
//
// Lookup
//
// Resolves a class and pins it with a global ref. Generated code caches the
// result in a static, so a class is only ever looked up once. A local ref would
// go stale the moment the native frame returns.
class_ref :: proc(e: Env, name: cstring) -> android.jclass {
local := e^^.FindClass(e, name)
if local == nil || failed(e) {
return nil
}
global := e^^.NewGlobalRef(e, android.jobject(local))
e^^.DeleteLocalRef(e, android.jobject(local))
return android.jclass(global)
}
method_id :: proc(e: Env, cls: android.jclass, name, sig: cstring) -> android.jmethodID {
id := e^^.GetMethodID(e, cls, name, sig)
if failed(e) {
return nil
}
return id
}
static_method_id :: proc(e: Env, cls: android.jclass, name, sig: cstring) -> android.jmethodID {
id := e^^.GetStaticMethodID(e, cls, name, sig)
if failed(e) {
return nil
}
return id
}
field_id :: proc(e: Env, cls: android.jclass, name, sig: cstring) -> android.jfieldID {
id := e^^.GetFieldID(e, cls, name, sig)
if failed(e) {
return nil
}
return id
}
static_field_id :: proc(e: Env, cls: android.jclass, name, sig: cstring) -> android.jfieldID {
id := e^^.GetStaticFieldID(e, cls, name, sig)
if failed(e) {
return nil
}
return id
}
free_local :: proc(e: Env, obj: android.jobject) {
if obj != nil {
e^^.DeleteLocalRef(e, obj)
}
}
//
// Strings
//
to_jstring :: proc(e: Env, s: string) -> android.jstring {
c := strings.clone_to_cstring(s, context.temp_allocator)
return e^^.NewStringUTF(e, c)
}
from_jstring :: proc(e: Env, s: android.jstring, allocator := context.allocator) -> string {
if s == nil {
return ""
}
utf := e^^.GetStringUTFChars(e, s, nil)
if utf == nil {
return ""
}
out := strings.clone(string(utf), allocator)
e^^.ReleaseStringUTFChars(e, s, utf)
return out
}
//
// Local reference frames
//
// Every object returned by a binding is a LOCAL ref, and locals are only
// reclaimed when control returns to Java -- which never happens on a native
// thread that owns its own loop. A sequence of object-returning calls made
// per-frame will therefore exhaust the local ref table (512 entries) and abort
// the process. Scope such a sequence in a frame:
//
// if !jni.push_frame(env) { return false }
// defer jni.pop_frame(env)
//
// Anything still needed after pop_frame must be a primitive, a copied string,
// or passed to pop_frame as `keep` to be promoted into the outer frame.
//
push_frame :: proc(e: Env, capacity: i32 = 16) -> bool {
if e == nil {
return false
}
return e^^.PushLocalFrame(e, android.jint(capacity)) == android.JNI_OK
}
pop_frame :: proc(e: Env, keep: android.jobject = nil) -> android.jobject {
if e == nil {
return nil
}
return e^^.PopLocalFrame(e, keep)
}
+901
View File
@@ -0,0 +1,901 @@
// GENERATED by jnigen from the JNI spec. Do not edit.
//
// Every binding returns `ok` -- false means the class, method or field
// failed to resolve, or the call raised a Java exception (already printed
// to logcat and cleared). Class and method IDs resolve once and are cached.
package main
import "shared:android"
import "jni"
//
// Object types
//
// android/content/Context
Context :: distinct android.jobject
// android/widget/Toast
Toast :: distinct android.jobject
// java/lang/StringBuilder
StringBuilder :: distinct android.jobject
// android/os/Looper
Looper :: distinct android.jobject
// android/app/Activity
Activity :: distinct android.jobject
// android/view/Window
Window :: distinct android.jobject
// android/view/View
View :: distinct android.jobject
// android/view/WindowInsets
WindowInsets :: distinct android.jobject
// android/graphics/Insets
Insets :: distinct android.jobject
// java/lang/System.currentTimeMillis()J
system_current_time_millis :: proc() -> (result: i64, ok: bool) {
env := jni.env()
if env == nil {
return
}
@(static) cls: android.jclass
if cls == nil {
cls = jni.class_ref(env, "java/lang/System")
if cls == nil {
return
}
}
@(static) mid: android.jmethodID
if mid == nil {
mid = jni.static_method_id(env, cls, "currentTimeMillis", "()J")
if mid == nil {
return
}
}
_ret := env^^.CallStaticLongMethodA(env, cls, mid, nil)
if jni.failed(env) {
return
}
result = i64(_ret)
ok = true
return
}
// android/widget/Toast.makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;
toast_make_text :: proc(ctx: Context, text: string, duration: i32) -> (result: Toast, ok: bool) {
env := jni.env()
if env == nil {
return
}
@(static) cls: android.jclass
if cls == nil {
cls = jni.class_ref(env, "android/widget/Toast")
if cls == nil {
return
}
}
@(static) mid: android.jmethodID
if mid == nil {
mid = jni.static_method_id(env, cls, "makeText", "(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;")
if mid == nil {
return
}
}
args := [3]android.jvalue{}
args[0].l = android.jobject(ctx)
_a1 := jni.to_jstring(env, text)
defer jni.free_local(env, android.jobject(_a1))
args[1].l = android.jobject(_a1)
args[2].i = android.jint(duration)
_ret := env^^.CallStaticObjectMethodA(env, cls, mid, raw_data(args[:]))
if jni.failed(env) {
return
}
result = Toast(_ret)
ok = true
return
}
// android/os/Looper.prepare()V
looper_prepare :: proc() -> (ok: bool) {
env := jni.env()
if env == nil {
return
}
@(static) cls: android.jclass
if cls == nil {
cls = jni.class_ref(env, "android/os/Looper")
if cls == nil {
return
}
}
@(static) mid: android.jmethodID
if mid == nil {
mid = jni.static_method_id(env, cls, "prepare", "()V")
if mid == nil {
return
}
}
env^^.CallStaticVoidMethodA(env, cls, mid, nil)
if jni.failed(env) {
return
}
ok = true
return
}
// android/os/Looper.myLooper()Landroid/os/Looper;
looper_my_looper :: proc() -> (result: Looper, ok: bool) {
env := jni.env()
if env == nil {
return
}
@(static) cls: android.jclass
if cls == nil {
cls = jni.class_ref(env, "android/os/Looper")
if cls == nil {
return
}
}
@(static) mid: android.jmethodID
if mid == nil {
mid = jni.static_method_id(env, cls, "myLooper", "()Landroid/os/Looper;")
if mid == nil {
return
}
}
_ret := env^^.CallStaticObjectMethodA(env, cls, mid, nil)
if jni.failed(env) {
return
}
result = Looper(_ret)
ok = true
return
}
// android/widget/Toast.show()V
toast_show :: proc(self: Toast) -> (ok: bool) {
env := jni.env()
if env == nil {
return
}
@(static) cls: android.jclass
if cls == nil {
cls = jni.class_ref(env, "android/widget/Toast")
if cls == nil {
return
}
}
@(static) mid: android.jmethodID
if mid == nil {
mid = jni.method_id(env, cls, "show", "()V")
if mid == nil {
return
}
}
env^^.CallVoidMethodA(env, android.jobject(self), mid, nil)
if jni.failed(env) {
return
}
ok = true
return
}
// java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder;
sb_append :: proc(self: StringBuilder, s: string) -> (result: StringBuilder, ok: bool) {
env := jni.env()
if env == nil {
return
}
@(static) cls: android.jclass
if cls == nil {
cls = jni.class_ref(env, "java/lang/StringBuilder")
if cls == nil {
return
}
}
@(static) mid: android.jmethodID
if mid == nil {
mid = jni.method_id(env, cls, "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;")
if mid == nil {
return
}
}
args := [1]android.jvalue{}
_a0 := jni.to_jstring(env, s)
defer jni.free_local(env, android.jobject(_a0))
args[0].l = android.jobject(_a0)
_ret := env^^.CallObjectMethodA(env, android.jobject(self), mid, raw_data(args[:]))
if jni.failed(env) {
return
}
result = StringBuilder(_ret)
ok = true
return
}
// java/lang/StringBuilder.toString()Ljava/lang/String;
sb_to_string :: proc(self: StringBuilder, allocator := context.allocator) -> (result: string, ok: bool) {
env := jni.env()
if env == nil {
return
}
@(static) cls: android.jclass
if cls == nil {
cls = jni.class_ref(env, "java/lang/StringBuilder")
if cls == nil {
return
}
}
@(static) mid: android.jmethodID
if mid == nil {
mid = jni.method_id(env, cls, "toString", "()Ljava/lang/String;")
if mid == nil {
return
}
}
_ret := env^^.CallObjectMethodA(env, android.jobject(self), mid, nil)
if jni.failed(env) {
return
}
defer jni.free_local(env, _ret)
result = jni.from_jstring(env, android.jstring(_ret), allocator)
ok = true
return
}
// new java/lang/StringBuilder()V
sb_new :: proc() -> (result: StringBuilder, ok: bool) {
env := jni.env()
if env == nil {
return
}
@(static) cls: android.jclass
if cls == nil {
cls = jni.class_ref(env, "java/lang/StringBuilder")
if cls == nil {
return
}
}
@(static) mid: android.jmethodID
if mid == nil {
mid = jni.method_id(env, cls, "<init>", "()V")
if mid == nil {
return
}
}
_ret := env^^.NewObjectA(env, cls, mid, nil)
if jni.failed(env) {
return
}
result = StringBuilder(_ret)
ok = true
return
}
// android/os/Build$VERSION.SDK_INT : I (get)
build_sdk_int :: proc() -> (result: i32, ok: bool) {
env := jni.env()
if env == nil {
return
}
@(static) cls: android.jclass
if cls == nil {
cls = jni.class_ref(env, "android/os/Build$VERSION")
if cls == nil {
return
}
}
@(static) fid: android.jfieldID
if fid == nil {
fid = jni.static_field_id(env, cls, "SDK_INT", "I")
if fid == nil {
return
}
}
_ret := env^^.GetStaticIntField(env, cls, fid)
if jni.failed(env) {
return
}
result = i32(_ret)
ok = true
return
}
// android/os/Build.MODEL : Ljava/lang/String; (get)
build_model :: proc(allocator := context.allocator) -> (result: string, ok: bool) {
env := jni.env()
if env == nil {
return
}
@(static) cls: android.jclass
if cls == nil {
cls = jni.class_ref(env, "android/os/Build")
if cls == nil {
return
}
}
@(static) fid: android.jfieldID
if fid == nil {
fid = jni.static_field_id(env, cls, "MODEL", "Ljava/lang/String;")
if fid == nil {
return
}
}
_ret := env^^.GetStaticObjectField(env, cls, fid)
if jni.failed(env) {
return
}
defer jni.free_local(env, _ret)
result = jni.from_jstring(env, android.jstring(_ret), allocator)
ok = true
return
}
// java/util/Arrays.sort([I)V
arrays_sort :: proc(vals: []i32) -> (ok: bool) {
env := jni.env()
if env == nil {
return
}
@(static) cls: android.jclass
if cls == nil {
cls = jni.class_ref(env, "java/util/Arrays")
if cls == nil {
return
}
}
@(static) mid: android.jmethodID
if mid == nil {
mid = jni.static_method_id(env, cls, "sort", "([I)V")
if mid == nil {
return
}
}
args := [1]android.jvalue{}
_a0 := jni.to_i32_array(env, vals)
defer jni.free_local(env, android.jobject(_a0))
args[0].l = android.jobject(_a0)
env^^.CallStaticVoidMethodA(env, cls, mid, raw_data(args[:]))
if jni.failed(env) {
return
}
ok = true
return
}
// android/os/Build.SUPPORTED_ABIS : [Ljava/lang/String; (get)
build_supported_abis :: proc(allocator := context.allocator) -> (result: []string, ok: bool) {
env := jni.env()
if env == nil {
return
}
@(static) cls: android.jclass
if cls == nil {
cls = jni.class_ref(env, "android/os/Build")
if cls == nil {
return
}
}
@(static) fid: android.jfieldID
if fid == nil {
fid = jni.static_field_id(env, cls, "SUPPORTED_ABIS", "[Ljava/lang/String;")
if fid == nil {
return
}
}
_ret := env^^.GetStaticObjectField(env, cls, fid)
if jni.failed(env) {
return
}
defer jni.free_local(env, _ret)
result = jni.from_string_array(env, android.jobjectArray(_ret), allocator)
ok = true
return
}
// android/app/Activity.getWindow()Landroid/view/Window;
activity_get_window :: proc(self: Activity) -> (result: Window, ok: bool) {
env := jni.env()
if env == nil {
return
}
@(static) cls: android.jclass
if cls == nil {
cls = jni.class_ref(env, "android/app/Activity")
if cls == nil {
return
}
}
@(static) mid: android.jmethodID
if mid == nil {
mid = jni.method_id(env, cls, "getWindow", "()Landroid/view/Window;")
if mid == nil {
return
}
}
_ret := env^^.CallObjectMethodA(env, android.jobject(self), mid, nil)
if jni.failed(env) {
return
}
result = Window(_ret)
ok = true
return
}
// android/view/Window.getDecorView()Landroid/view/View;
window_get_decor_view :: proc(self: Window) -> (result: View, ok: bool) {
env := jni.env()
if env == nil {
return
}
@(static) cls: android.jclass
if cls == nil {
cls = jni.class_ref(env, "android/view/Window")
if cls == nil {
return
}
}
@(static) mid: android.jmethodID
if mid == nil {
mid = jni.method_id(env, cls, "getDecorView", "()Landroid/view/View;")
if mid == nil {
return
}
}
_ret := env^^.CallObjectMethodA(env, android.jobject(self), mid, nil)
if jni.failed(env) {
return
}
result = View(_ret)
ok = true
return
}
// android/view/View.getRootWindowInsets()Landroid/view/WindowInsets;
view_get_root_window_insets :: proc(self: View) -> (result: WindowInsets, ok: bool) {
env := jni.env()
if env == nil {
return
}
@(static) cls: android.jclass
if cls == nil {
cls = jni.class_ref(env, "android/view/View")
if cls == nil {
return
}
}
@(static) mid: android.jmethodID
if mid == nil {
mid = jni.method_id(env, cls, "getRootWindowInsets", "()Landroid/view/WindowInsets;")
if mid == nil {
return
}
}
_ret := env^^.CallObjectMethodA(env, android.jobject(self), mid, nil)
if jni.failed(env) {
return
}
result = WindowInsets(_ret)
ok = true
return
}
// android/view/WindowInsets$Type.systemBars()I
insets_type_system_bars :: proc() -> (result: i32, ok: bool) {
env := jni.env()
if env == nil {
return
}
@(static) cls: android.jclass
if cls == nil {
cls = jni.class_ref(env, "android/view/WindowInsets$Type")
if cls == nil {
return
}
}
@(static) mid: android.jmethodID
if mid == nil {
mid = jni.static_method_id(env, cls, "systemBars", "()I")
if mid == nil {
return
}
}
_ret := env^^.CallStaticIntMethodA(env, cls, mid, nil)
if jni.failed(env) {
return
}
result = i32(_ret)
ok = true
return
}
// android/view/WindowInsets$Type.displayCutout()I
insets_type_display_cutout :: proc() -> (result: i32, ok: bool) {
env := jni.env()
if env == nil {
return
}
@(static) cls: android.jclass
if cls == nil {
cls = jni.class_ref(env, "android/view/WindowInsets$Type")
if cls == nil {
return
}
}
@(static) mid: android.jmethodID
if mid == nil {
mid = jni.static_method_id(env, cls, "displayCutout", "()I")
if mid == nil {
return
}
}
_ret := env^^.CallStaticIntMethodA(env, cls, mid, nil)
if jni.failed(env) {
return
}
result = i32(_ret)
ok = true
return
}
// android/view/WindowInsets.getInsetsIgnoringVisibility(I)Landroid/graphics/Insets;
window_insets_get_ignoring_visibility :: proc(self: WindowInsets, type_mask: i32) -> (result: Insets, ok: bool) {
env := jni.env()
if env == nil {
return
}
@(static) cls: android.jclass
if cls == nil {
cls = jni.class_ref(env, "android/view/WindowInsets")
if cls == nil {
return
}
}
@(static) mid: android.jmethodID
if mid == nil {
mid = jni.method_id(env, cls, "getInsetsIgnoringVisibility", "(I)Landroid/graphics/Insets;")
if mid == nil {
return
}
}
args := [1]android.jvalue{}
args[0].i = android.jint(type_mask)
_ret := env^^.CallObjectMethodA(env, android.jobject(self), mid, raw_data(args[:]))
if jni.failed(env) {
return
}
result = Insets(_ret)
ok = true
return
}
// android/view/WindowInsets.getInsets(I)Landroid/graphics/Insets;
window_insets_get :: proc(self: WindowInsets, type_mask: i32) -> (result: Insets, ok: bool) {
env := jni.env()
if env == nil {
return
}
@(static) cls: android.jclass
if cls == nil {
cls = jni.class_ref(env, "android/view/WindowInsets")
if cls == nil {
return
}
}
@(static) mid: android.jmethodID
if mid == nil {
mid = jni.method_id(env, cls, "getInsets", "(I)Landroid/graphics/Insets;")
if mid == nil {
return
}
}
args := [1]android.jvalue{}
args[0].i = android.jint(type_mask)
_ret := env^^.CallObjectMethodA(env, android.jobject(self), mid, raw_data(args[:]))
if jni.failed(env) {
return
}
result = Insets(_ret)
ok = true
return
}
// android/graphics/Insets.left : I (get)
insets_left :: proc(self: Insets) -> (result: i32, ok: bool) {
env := jni.env()
if env == nil {
return
}
@(static) cls: android.jclass
if cls == nil {
cls = jni.class_ref(env, "android/graphics/Insets")
if cls == nil {
return
}
}
@(static) fid: android.jfieldID
if fid == nil {
fid = jni.field_id(env, cls, "left", "I")
if fid == nil {
return
}
}
_ret := env^^.GetIntField(env, android.jobject(self), fid)
if jni.failed(env) {
return
}
result = i32(_ret)
ok = true
return
}
// android/graphics/Insets.top : I (get)
insets_top :: proc(self: Insets) -> (result: i32, ok: bool) {
env := jni.env()
if env == nil {
return
}
@(static) cls: android.jclass
if cls == nil {
cls = jni.class_ref(env, "android/graphics/Insets")
if cls == nil {
return
}
}
@(static) fid: android.jfieldID
if fid == nil {
fid = jni.field_id(env, cls, "top", "I")
if fid == nil {
return
}
}
_ret := env^^.GetIntField(env, android.jobject(self), fid)
if jni.failed(env) {
return
}
result = i32(_ret)
ok = true
return
}
// android/graphics/Insets.right : I (get)
insets_right :: proc(self: Insets) -> (result: i32, ok: bool) {
env := jni.env()
if env == nil {
return
}
@(static) cls: android.jclass
if cls == nil {
cls = jni.class_ref(env, "android/graphics/Insets")
if cls == nil {
return
}
}
@(static) fid: android.jfieldID
if fid == nil {
fid = jni.field_id(env, cls, "right", "I")
if fid == nil {
return
}
}
_ret := env^^.GetIntField(env, android.jobject(self), fid)
if jni.failed(env) {
return
}
result = i32(_ret)
ok = true
return
}
// android/graphics/Insets.bottom : I (get)
insets_bottom :: proc(self: Insets) -> (result: i32, ok: bool) {
env := jni.env()
if env == nil {
return
}
@(static) cls: android.jclass
if cls == nil {
cls = jni.class_ref(env, "android/graphics/Insets")
if cls == nil {
return
}
}
@(static) fid: android.jfieldID
if fid == nil {
fid = jni.field_id(env, cls, "bottom", "I")
if fid == nil {
return
}
}
_ret := env^^.GetIntField(env, android.jobject(self), fid)
if jni.failed(env) {
return
}
result = i32(_ret)
ok = true
return
}
// android/view/WindowInsets.getSystemWindowInsetLeft()I
window_insets_sys_left :: proc(self: WindowInsets) -> (result: i32, ok: bool) {
env := jni.env()
if env == nil {
return
}
@(static) cls: android.jclass
if cls == nil {
cls = jni.class_ref(env, "android/view/WindowInsets")
if cls == nil {
return
}
}
@(static) mid: android.jmethodID
if mid == nil {
mid = jni.method_id(env, cls, "getSystemWindowInsetLeft", "()I")
if mid == nil {
return
}
}
_ret := env^^.CallIntMethodA(env, android.jobject(self), mid, nil)
if jni.failed(env) {
return
}
result = i32(_ret)
ok = true
return
}
// android/view/WindowInsets.getSystemWindowInsetTop()I
window_insets_sys_top :: proc(self: WindowInsets) -> (result: i32, ok: bool) {
env := jni.env()
if env == nil {
return
}
@(static) cls: android.jclass
if cls == nil {
cls = jni.class_ref(env, "android/view/WindowInsets")
if cls == nil {
return
}
}
@(static) mid: android.jmethodID
if mid == nil {
mid = jni.method_id(env, cls, "getSystemWindowInsetTop", "()I")
if mid == nil {
return
}
}
_ret := env^^.CallIntMethodA(env, android.jobject(self), mid, nil)
if jni.failed(env) {
return
}
result = i32(_ret)
ok = true
return
}
// android/view/WindowInsets.getSystemWindowInsetRight()I
window_insets_sys_right :: proc(self: WindowInsets) -> (result: i32, ok: bool) {
env := jni.env()
if env == nil {
return
}
@(static) cls: android.jclass
if cls == nil {
cls = jni.class_ref(env, "android/view/WindowInsets")
if cls == nil {
return
}
}
@(static) mid: android.jmethodID
if mid == nil {
mid = jni.method_id(env, cls, "getSystemWindowInsetRight", "()I")
if mid == nil {
return
}
}
_ret := env^^.CallIntMethodA(env, android.jobject(self), mid, nil)
if jni.failed(env) {
return
}
result = i32(_ret)
ok = true
return
}
// android/view/WindowInsets.getSystemWindowInsetBottom()I
window_insets_sys_bottom :: proc(self: WindowInsets) -> (result: i32, ok: bool) {
env := jni.env()
if env == nil {
return
}
@(static) cls: android.jclass
if cls == nil {
cls = jni.class_ref(env, "android/view/WindowInsets")
if cls == nil {
return
}
}
@(static) mid: android.jmethodID
if mid == nil {
mid = jni.method_id(env, cls, "getSystemWindowInsetBottom", "()I")
if mid == nil {
return
}
}
_ret := env^^.CallIntMethodA(env, android.jobject(self), mid, nil)
if jni.failed(env) {
return
}
result = i32(_ret)
ok = true
return
}
+175
View File
@@ -0,0 +1,175 @@
#+build ignore
// JNI binding spec. Never compiled -- jnigen parses this file and emits
// jni_generated.odin. Regenerate with:
//
// odin run jnigen -- jni_spec.odin jni_generated.odin main
//
// Attributes:
// java_class="a/b/C" required; the class, or the type's Java class
// java_name="foo" Java-side name (defaults to the Odin proc name)
// java_static static method or field
// java_ctor constructor; must return a registered object type
// java_field field access; 1 result = getter, 0 results = setter
// java_sig="..." override the computed signature
package main
import "shared:android"
//
// Object types. Each becomes `Name :: distinct android.jobject` and lets
// signatures resolve the Odin type to its Java class.
//
@(java_class = "android/content/Context")
Context :: distinct android.jobject
@(java_class = "android/widget/Toast")
Toast :: distinct android.jobject
@(java_class = "java/lang/StringBuilder")
StringBuilder :: distinct android.jobject
@(java_class = "android/os/Looper")
Looper :: distinct android.jobject
@(java_class = "android/app/Activity")
Activity :: distinct android.jobject
@(java_class = "android/view/Window")
Window :: distinct android.jobject
@(java_class = "android/view/View")
View :: distinct android.jobject
@(java_class = "android/view/WindowInsets")
WindowInsets :: distinct android.jobject
@(java_class = "android/graphics/Insets")
Insets :: distinct android.jobject
//
// Static methods
//
@(java_class = "java/lang/System", java_static, java_name = "currentTimeMillis")
system_current_time_millis :: proc() -> i64
// makeText takes a CharSequence, not a String, so the computed signature would
// not match. java_sig pins the real one while `string` stays in the Odin API.
@(
java_class = "android/widget/Toast",
java_static,
java_name = "makeText",
java_sig = "(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;",
)
toast_make_text :: proc(ctx: Context, text: string, duration: i32) -> Toast
// Toast's constructor reads Looper.myLooper() and throws if the thread has
// none. The engine loop runs on a glue-spawned thread that only ever got a
// native ALooper, so the Java-side looper has to be prepared once by hand.
@(java_class = "android/os/Looper", java_static, java_name = "prepare")
looper_prepare :: proc()
@(java_class = "android/os/Looper", java_static, java_name = "myLooper")
looper_my_looper :: proc() -> Looper
//
// Instance methods -- the first parameter is the receiver.
//
@(java_class = "android/widget/Toast", java_name = "show")
toast_show :: proc(self: Toast)
@(java_class = "java/lang/StringBuilder", java_name = "append", java_sig = "(Ljava/lang/String;)Ljava/lang/StringBuilder;")
sb_append :: proc(self: StringBuilder, s: string) -> StringBuilder
@(java_class = "java/lang/StringBuilder", java_name = "toString")
sb_to_string :: proc(self: StringBuilder) -> string
//
// Constructor
//
@(java_class = "java/lang/StringBuilder", java_ctor)
sb_new :: proc() -> StringBuilder
//
// Fields
//
@(java_class = "android/os/Build$VERSION", java_static, java_field, java_name = "SDK_INT")
build_sdk_int :: proc() -> i32
@(java_class = "android/os/Build", java_static, java_field, java_name = "MODEL")
build_model :: proc() -> string
//
// Arrays
//
@(java_class = "java/util/Arrays", java_static, java_name = "sort", java_sig = "([I)V")
arrays_sort :: proc(vals: []i32)
@(java_class = "android/os/Build", java_static, java_field, java_name = "SUPPORTED_ABIS")
build_supported_abis :: proc() -> []string
//
// Window insets: Activity -> Window -> decor View -> WindowInsets -> Insets.
//
// getRootWindowInsets returns null until the decor view is attached, so the
// first frames after launch will come back nil -- retry rather than treating
// it as an error.
//
@(java_class = "android/app/Activity", java_name = "getWindow")
activity_get_window :: proc(self: Activity) -> Window
@(java_class = "android/view/Window", java_name = "getDecorView")
window_get_decor_view :: proc(self: Window) -> View
@(java_class = "android/view/View", java_name = "getRootWindowInsets")
view_get_root_window_insets :: proc(self: View) -> WindowInsets
// WindowInsets.Type is a nested class, hence the $ separator.
@(java_class = "android/view/WindowInsets$Type", java_static, java_name = "systemBars")
insets_type_system_bars :: proc() -> i32
// A fullscreen theme hides the system bars, so systemBars() alone reports 0.
// The cutout is what still needs avoiding -- the two are OR'd into one mask.
@(java_class = "android/view/WindowInsets$Type", java_static, java_name = "displayCutout")
insets_type_display_cutout :: proc() -> i32
// Reports bar sizes even while hidden, which is what a fullscreen app wants
// when it draws its own chrome where the bars would otherwise sit.
@(java_class = "android/view/WindowInsets", java_name = "getInsetsIgnoringVisibility")
window_insets_get_ignoring_visibility :: proc(self: WindowInsets, type_mask: i32) -> Insets
// API 30+. The computed signature (I)Landroid/graphics/Insets; is already right.
@(java_class = "android/view/WindowInsets", java_name = "getInsets")
window_insets_get :: proc(self: WindowInsets, type_mask: i32) -> Insets
// Insets exposes its edges as public final int fields.
@(java_class = "android/graphics/Insets", java_field, java_name = "left")
insets_left :: proc(self: Insets) -> i32
@(java_class = "android/graphics/Insets", java_field, java_name = "top")
insets_top :: proc(self: Insets) -> i32
@(java_class = "android/graphics/Insets", java_field, java_name = "right")
insets_right :: proc(self: Insets) -> i32
@(java_class = "android/graphics/Insets", java_field, java_name = "bottom")
insets_bottom :: proc(self: Insets) -> i32
// Deprecated in API 30 but present since 20 -- the fallback below minSdk 30.
@(java_class = "android/view/WindowInsets", java_name = "getSystemWindowInsetLeft")
window_insets_sys_left :: proc(self: WindowInsets) -> i32
@(java_class = "android/view/WindowInsets", java_name = "getSystemWindowInsetTop")
window_insets_sys_top :: proc(self: WindowInsets) -> i32
@(java_class = "android/view/WindowInsets", java_name = "getSystemWindowInsetRight")
window_insets_sys_right :: proc(self: WindowInsets) -> i32
@(java_class = "android/view/WindowInsets", java_name = "getSystemWindowInsetBottom")
window_insets_sys_bottom :: proc(self: WindowInsets) -> i32
+296
View File
@@ -0,0 +1,296 @@
package jnigen
import "core:fmt"
import "core:strings"
Emitter :: struct {
b: strings.Builder,
}
w :: proc(e: ^Emitter, format: string, args: ..any) {
fmt.sbprintf(&e.b, format, ..args)
strings.write_byte(&e.b, '\n')
}
emit_file :: proc(g: ^Gen, package_name: string) -> string {
e: Emitter
e.b = strings.builder_make()
w(&e, "// GENERATED by jnigen from the JNI spec. Do not edit.")
w(&e, "//")
w(&e, "// Every binding returns `ok` -- false means the class, method or field")
w(&e, "// failed to resolve, or the call raised a Java exception (already printed")
w(&e, "// to logcat and cleared). Class and method IDs resolve once and are cached.")
w(&e, "package %s", package_name)
w(&e, "")
w(&e, "import \"shared:android\"")
w(&e, "import \"jni\"")
w(&e, "")
if len(g.class_decls) > 0 {
w(&e, "//")
w(&e, "// Object types")
w(&e, "//")
w(&e, "")
for c in g.class_decls {
w(&e, "// %s", c.java_class)
w(&e, "%s :: distinct android.jobject", c.odin_name)
w(&e, "")
}
}
for b in g.bindings {
emit_binding(&e, b)
}
return strings.to_string(e.b)
}
@(private = "file")
emit_signature :: proc(e: ^Emitter, b: Binding) {
sb := strings.builder_make(context.temp_allocator)
if b.has_recv {
fmt.sbprintf(&sb, "%s: %s", b.recv.name, b.recv.type.odin)
}
for p in b.params {
if strings.builder_len(sb) > 0 {
strings.write_string(&sb, ", ")
}
fmt.sbprintf(&sb, "%s: %s", p.name, p.type.odin)
}
if allocates(b.result) {
if strings.builder_len(sb) > 0 {
strings.write_string(&sb, ", ")
}
strings.write_string(&sb, "allocator := context.allocator")
}
if b.result.of == .Void {
w(e, "%s :: proc(%s) -> (ok: bool) {{", b.odin_name, strings.to_string(sb))
} else {
w(e, "%s :: proc(%s) -> (result: %s, ok: bool) {{", b.odin_name, strings.to_string(sb), b.result.odin)
}
}
@(private = "file")
emit_binding :: proc(e: ^Emitter, b: Binding) {
switch b.kind {
case .Method:
w(e, "// %s.%s%s", b.java_class, b.java_name, b.sig)
case .Ctor:
w(e, "// new %s%s", b.java_class, b.sig)
case .Field_Get:
w(e, "// %s.%s : %s (get)", b.java_class, b.java_name, b.sig)
case .Field_Set:
w(e, "// %s.%s : %s (set)", b.java_class, b.java_name, b.sig)
}
emit_signature(e, b)
w(e, " env := jni.env()")
w(e, " if env == nil {{")
w(e, " return")
w(e, " }}")
w(e, "")
// Resolved once; a global ref keeps the class alive past this frame.
w(e, " @(static) cls: android.jclass")
w(e, " if cls == nil {{")
w(e, " cls = jni.class_ref(env, \"%s\")", b.java_class)
w(e, " if cls == nil {{")
w(e, " return")
w(e, " }}")
w(e, " }}")
is_field := b.kind == .Field_Get || b.kind == .Field_Set
if is_field {
lookup := b.is_static ? "static_field_id" : "field_id"
w(e, " @(static) fid: android.jfieldID")
w(e, " if fid == nil {{")
w(e, " fid = jni.%s(env, cls, \"%s\", \"%s\")", lookup, b.java_name, b.sig)
w(e, " if fid == nil {{")
w(e, " return")
w(e, " }}")
w(e, " }}")
} else {
lookup := (b.is_static && b.kind != .Ctor) ? "static_method_id" : "method_id"
w(e, " @(static) mid: android.jmethodID")
w(e, " if mid == nil {{")
w(e, " mid = jni.%s(env, cls, \"%s\", \"%s\")", lookup, b.java_name, b.sig)
w(e, " if mid == nil {{")
w(e, " return")
w(e, " }}")
w(e, " }}")
}
w(e, "")
switch b.kind {
case .Method, .Ctor:
emit_call(e, b)
case .Field_Get:
emit_field_get(e, b)
case .Field_Set:
emit_field_set(e, b)
}
w(e, "}}")
w(e, "")
}
// Marshals one argument into `args[idx]`, emitting any temporary the value needs.
@(private = "file")
emit_marshal :: proc(e: ^Emitter, p: Param, idx: int) {
t := p.type
switch t.of {
case .Prim:
w(e, " args[%d].%s = %s(%s)", idx, t.jval, t.jcast, p.name)
case .String:
w(e, " _a%d := jni.to_jstring(env, %s)", idx, p.name)
w(e, " defer jni.free_local(env, android.jobject(_a%d))", idx)
w(e, " args[%d].l = android.jobject(_a%d)", idx, idx)
case .Array:
conv := t.elem == "string" ? "to_string_array" : fmt.tprintf("to_%s_array", t.elem)
w(e, " _a%d := jni.%s(env, %s)", idx, conv, p.name)
w(e, " defer jni.free_local(env, android.jobject(_a%d))", idx)
w(e, " args[%d].l = android.jobject(_a%d)", idx, idx)
case .Object:
w(e, " args[%d].l = android.jobject(%s)", idx, p.name)
case .Void:
}
}
@(private = "file")
emit_call :: proc(e: ^Emitter, b: Binding) {
args_expr := "nil"
if len(b.params) > 0 {
w(e, " args := [%d]android.jvalue{{}}", len(b.params))
for p, i in b.params {
emit_marshal(e, p, i)
}
w(e, "")
args_expr = "raw_data(args[:])"
}
kind := title(b.result.call)
switch {
case b.kind == .Ctor:
w(e, " _ret := env^^.NewObjectA(env, cls, mid, %s)", args_expr)
case b.result.of == .Void && b.is_static:
// A void call has no value to bind.
w(e, " env^^.CallStaticVoidMethodA(env, cls, mid, %s)", args_expr)
case b.result.of == .Void:
w(e, " env^^.CallVoidMethodA(env, android.jobject(%s), mid, %s)", b.recv.name, args_expr)
case b.is_static:
w(e, " _ret := env^^.CallStatic%sMethodA(env, cls, mid, %s)", kind, args_expr)
case:
w(e, " _ret := env^^.Call%sMethodA(env, android.jobject(%s), mid, %s)", kind, b.recv.name, args_expr)
}
w(e, " if jni.failed(env) {{")
w(e, " return")
w(e, " }}")
emit_result(e, b, "_ret")
w(e, " ok = true")
w(e, " return")
}
@(private = "file")
emit_result :: proc(e: ^Emitter, b: Binding, ret: string) {
t := b.result
switch t.of {
case .Void:
case .Prim:
w(e, " result = %s(%s)", t.odin, ret)
case .String:
w(e, " defer jni.free_local(env, %s)", ret)
w(e, " result = jni.from_jstring(env, android.jstring(%s), allocator)", ret)
case .Array:
conv := t.elem == "string" ? "from_string_array" : fmt.tprintf("from_%s_array", t.elem)
jarr := t.elem == "string" ? "android.jobjectArray" : fmt.tprintf("android.j%sArray", java_arr_name(t.elem))
w(e, " defer jni.free_local(env, %s)", ret)
w(e, " result = jni.%s(env, %s(%s), allocator)", conv, jarr, ret)
case .Object:
w(e, " result = %s(%s)", t.odin, ret)
}
}
// Odin element type -> the jni.odin array type infix (jintArray, jlongArray...).
java_arr_name :: proc(elem: string) -> string {
for p in prims {
if p.odin == elem {
return p.call
}
}
return "object"
}
@(private = "file")
emit_field_get :: proc(e: ^Emitter, b: Binding) {
t := b.result
getter := field_accessor(t, b.is_static, true)
target := b.is_static ? "cls" : fmt.tprintf("android.jobject(%s)", b.recv.name)
w(e, " _ret := env^^.%s(env, %s, fid)", getter, target)
w(e, " if jni.failed(env) {{")
w(e, " return")
w(e, " }}")
emit_result(e, b, "_ret")
w(e, " ok = true")
w(e, " return")
}
@(private = "file")
emit_field_set :: proc(e: ^Emitter, b: Binding) {
p := b.params[0]
setter := field_accessor(p.type, b.is_static, false)
target := b.is_static ? "cls" : fmt.tprintf("android.jobject(%s)", b.recv.name)
value := ""
switch p.type.of {
case .Prim:
value = fmt.tprintf("%s(%s)", p.type.jcast, p.name)
case .String:
w(e, " _v := jni.to_jstring(env, %s)", p.name)
w(e, " defer jni.free_local(env, android.jobject(_v))")
value = "android.jobject(_v)"
case .Array:
conv := p.type.elem == "string" ? "to_string_array" : fmt.tprintf("to_%s_array", p.type.elem)
w(e, " _v := jni.%s(env, %s)", conv, p.name)
w(e, " defer jni.free_local(env, android.jobject(_v))")
value = "android.jobject(_v)"
case .Object:
value = fmt.tprintf("android.jobject(%s)", p.name)
case .Void:
}
w(e, " env^^.%s(env, %s, fid, %s)", setter, target, value)
w(e, " if jni.failed(env) {{")
w(e, " return")
w(e, " }}")
w(e, " ok = true")
w(e, " return")
}
// GetStaticIntField / SetObjectField / ...
field_accessor :: proc(t: Type_Ref, is_static: bool, is_get: bool) -> string {
kind := "Object"
if t.of == .Prim {
for p in prims {
if p.odin == t.odin {
kind = title(p.call)
break
}
}
}
verb := is_get ? "Get" : "Set"
if is_static {
return fmt.tprintf("%sStatic%sField", verb, kind)
}
return fmt.tprintf("%s%sField", verb, kind)
}
title :: proc(s: string) -> string {
if len(s) == 0 {
return s
}
return fmt.tprintf("%c%s", s[0] - 32, s[1:])
}
+72
View File
@@ -0,0 +1,72 @@
// jnigen -- generates JNI binding boilerplate from an annotated Odin spec.
//
// odin run jnigen -- [spec.odin] [out.odin] [package]
//
// The spec is never compiled (it carries `#+build ignore`), so it needs no
// -custom-attribute flags and cannot collide with the generated procs.
package jnigen
import "core:fmt"
import "core:odin/ast"
import "core:odin/parser"
import "core:os"
main :: proc() {
spec_path := "jni_spec.odin"
out_path := "jni_generated.odin"
package_name := "main"
args := os.args[1:]
if len(args) > 0 {
spec_path = args[0]
}
if len(args) > 1 {
out_path = args[1]
}
if len(args) > 2 {
package_name = args[2]
}
src, read_err := os.read_entire_file(spec_path, context.allocator)
if read_err != nil {
fmt.eprintfln("jnigen: cannot read %s: %v", spec_path, read_err)
os.exit(1)
}
p := parser.default_parser()
f := ast.File{
src = string(src),
fullpath = spec_path,
}
if !parser.parse_file(&p, &f) {
fmt.eprintfln("jnigen: failed to parse %s", spec_path)
os.exit(1)
}
g: Gen
g.classes = make(map[string]string)
collect_classes(&g, &f)
collect_bindings(&g, &f)
if len(g.errors) > 0 {
for e in g.errors {
fmt.eprintfln("jnigen: %s", e)
}
os.exit(1)
}
out := emit_file(&g, package_name)
if werr := os.write_entire_file(out_path, transmute([]byte)out); werr != nil {
fmt.eprintfln("jnigen: cannot write %s: %v", out_path, werr)
os.exit(1)
}
fmt.printfln(
"jnigen: %s -> %s (%d classes, %d bindings)",
spec_path,
out_path,
len(g.class_decls),
len(g.bindings),
)
}
+288
View File
@@ -0,0 +1,288 @@
package jnigen
import "core:fmt"
import "core:odin/ast"
import "core:strings"
Kind :: enum {
Method,
Ctor,
Field_Get,
Field_Set,
}
Class_Of :: enum {
Void,
Prim,
String,
Object,
Array,
}
Type_Ref :: struct {
odin: string, // Odin type as written in the spec
sig: string, // JNI signature fragment
of: Class_Of,
call: string, // "int" / "object" / "void" -- picks the jni.call_* wrapper
jval: string, // jvalue union field
jcast: string, // android.jint etc, for widening into the union
elem: string, // element type, arrays only
}
Param :: struct {
name: string,
type: Type_Ref,
}
Binding :: struct {
odin_name: string,
java_class: string,
java_name: string,
kind: Kind,
is_static: bool,
recv: Param,
has_recv: bool,
params: []Param,
result: Type_Ref,
sig: string,
line: int,
}
Class_Bind :: struct {
odin_name: string,
java_class: string,
}
Gen :: struct {
classes: map[string]string, // Odin type name -> java/class/path
class_decls: [dynamic]Class_Bind,
bindings: [dynamic]Binding,
errors: [dynamic]string,
}
errorf :: proc(g: ^Gen, line: int, format: string, args: ..any) {
msg := fmt.tprintf(format, ..args)
append(&g.errors, fmt.aprintf("spec:%d: %s", line, msg))
}
@(private = "file")
attr_map :: proc(f: ^ast.File, attrs: [dynamic]^ast.Attribute) -> map[string]string {
out := make(map[string]string)
for attr in attrs {
for elem in attr.elems {
#partial switch e in elem.derived {
case ^ast.Field_Value:
key := src_of(f, e.field)
val := strings.trim(src_of(f, e.value), "\"")
out[key] = val
case ^ast.Ident:
out[e.name] = ""
}
}
}
return out
}
src_of :: proc(f: ^ast.File, e: ^ast.Node) -> string {
if e == nil {
return ""
}
return strings.trim_space(f.src[e.pos.offset:e.end.offset])
}
// First pass: every `@(java_class=...) Name :: distinct jobject` becomes a
// registered object type, so later signatures can resolve `Toast` to
// `Landroid/widget/Toast;`.
collect_classes :: proc(g: ^Gen, f: ^ast.File) {
for decl in f.decls {
vd, ok := decl.derived.(^ast.Value_Decl)
if !ok || len(vd.attributes) == 0 || len(vd.values) == 0 {
continue
}
// A proc type/lit is a method binding, not a class registration.
#partial switch _ in vd.values[0].derived {
case ^ast.Proc_Type:
continue
case ^ast.Proc_Lit:
continue
}
attrs := attr_map(f, vd.attributes)
defer delete(attrs)
cls, has := attrs["java_class"]
if !has || len(vd.names) == 0 {
continue
}
name := src_of(f, vd.names[0])
g.classes[strings.clone(name)] = strings.clone(cls)
append(&g.class_decls, Class_Bind{strings.clone(name), strings.clone(cls)})
}
}
collect_bindings :: proc(g: ^Gen, f: ^ast.File) {
for decl in f.decls {
vd, ok := decl.derived.(^ast.Value_Decl)
if !ok || len(vd.attributes) == 0 || len(vd.values) == 0 || len(vd.names) == 0 {
continue
}
pt: ^ast.Proc_Type
#partial switch v in vd.values[0].derived {
case ^ast.Proc_Type:
pt = v
case ^ast.Proc_Lit:
pt = v.type
case:
continue
}
attrs := attr_map(f, vd.attributes)
defer delete(attrs)
java_class, has_class := attrs["java_class"]
if !has_class {
continue
}
b: Binding
b.line = vd.pos.line
b.odin_name = strings.clone(src_of(f, vd.names[0]))
b.java_class = strings.clone(java_class)
_, b.is_static = attrs["java_static"]
if name, has := attrs["java_name"]; has {
b.java_name = strings.clone(name)
} else {
b.java_name = b.odin_name
}
_, is_ctor := attrs["java_ctor"]
_, is_field := attrs["java_field"]
params := parse_params(g, f, pt, b.line)
results := parse_results(g, f, pt, b.line)
if len(results) > 1 {
errorf(g, b.line, "'%s' declares %d results; bindings return at most one value", b.odin_name, len(results))
continue
}
if len(results) == 1 {
b.result = results[0]
} else {
b.result = Type_Ref{odin = "", sig = "V", of = .Void, call = "void"}
}
switch {
case is_ctor:
b.kind = .Ctor
b.java_name = "<init>"
if b.result.of != .Object {
errorf(g, b.line, "'%s' is a constructor so it must return a registered object type", b.odin_name)
continue
}
b.params = params
b.sig = build_sig(params, Type_Ref{sig = "V", of = .Void})
case is_field:
if b.result.of == .Void {
b.kind = .Field_Set
// instance setter: (self, value); static setter: (value)
if !b.is_static {
if len(params) != 2 {
errorf(g, b.line, "'%s' is an instance field setter so it must take (self, value)", b.odin_name)
continue
}
b.recv, b.has_recv = params[0], true
b.params = params[1:]
} else {
if len(params) != 1 {
errorf(g, b.line, "'%s' is a static field setter so it must take exactly (value)", b.odin_name)
continue
}
b.params = params
}
b.sig = b.params[0].type.sig
} else {
b.kind = .Field_Get
if !b.is_static {
if len(params) != 1 {
errorf(g, b.line, "'%s' is an instance field getter so it must take exactly (self)", b.odin_name)
continue
}
b.recv, b.has_recv = params[0], true
} else if len(params) != 0 {
errorf(g, b.line, "'%s' is a static field getter so it must take no parameters", b.odin_name)
continue
}
b.params = nil
b.sig = b.result.sig
}
case:
b.kind = .Method
if !b.is_static {
if len(params) == 0 {
errorf(g, b.line, "'%s' is an instance method so its first parameter must be the receiver", b.odin_name)
continue
}
b.recv, b.has_recv = params[0], true
b.params = params[1:]
} else {
b.params = params
}
b.sig = build_sig(b.params, b.result)
}
if override, has := attrs["java_sig"]; has {
b.sig = strings.clone(override)
}
append(&g.bindings, b)
}
}
@(private = "file")
parse_params :: proc(g: ^Gen, f: ^ast.File, pt: ^ast.Proc_Type, line: int) -> []Param {
out := make([dynamic]Param)
if pt.params == nil {
return out[:]
}
for field in pt.params.list {
t, ok := resolve_type(g, src_of(f, field.type), line)
if !ok {
continue
}
// `a, b: i32` declares two params of one type.
if len(field.names) == 0 {
append(&out, Param{fmt.aprintf("_arg%d", len(out)), t})
continue
}
for n in field.names {
append(&out, Param{strings.clone(src_of(f, n)), t})
}
}
return out[:]
}
@(private = "file")
parse_results :: proc(g: ^Gen, f: ^ast.File, pt: ^ast.Proc_Type, line: int) -> []Type_Ref {
out := make([dynamic]Type_Ref)
if pt.results == nil {
return out[:]
}
for field in pt.results.list {
t, ok := resolve_type(g, src_of(f, field.type), line)
if ok {
append(&out, t)
}
}
return out[:]
}
build_sig :: proc(params: []Param, result: Type_Ref) -> string {
b := strings.builder_make()
strings.write_byte(&b, '(')
for p in params {
strings.write_string(&b, p.type.sig)
}
strings.write_byte(&b, ')')
strings.write_string(&b, result.sig)
return strings.to_string(b)
}
+89
View File
@@ -0,0 +1,89 @@
package jnigen
import "core:fmt"
import "core:strings"
Prim :: struct {
odin: string,
sig: string,
call: string,
jval: string,
jcast: string,
}
// Odin's u16 stands in for Java's char: both are unsigned 16-bit.
prims := []Prim{
{"bool", "Z", "boolean", "z", "android.jboolean"},
{"i8", "B", "byte", "b", "android.jbyte"},
{"u16", "C", "char", "c", "android.jchar"},
{"i16", "S", "short", "s", "android.jshort"},
{"i32", "I", "int", "i", "android.jint"},
{"i64", "J", "long", "j", "android.jlong"},
{"f32", "F", "float", "f", "android.jfloat"},
{"f64", "D", "double", "d", "android.jdouble"},
}
resolve_type :: proc(g: ^Gen, odin: string, line: int) -> (Type_Ref, bool) {
src := strings.trim_space(odin)
if src == "" {
return Type_Ref{sig = "V", of = .Void, call = "void"}, true
}
for p in prims {
if src == p.odin {
return Type_Ref{
odin = src, sig = p.sig, of = .Prim,
call = p.call, jval = p.jval, jcast = p.jcast,
}, true
}
}
if src == "string" || src == "cstring" {
return Type_Ref{
odin = src, sig = "Ljava/lang/String;", of = .String,
call = "object", jval = "l",
}, true
}
if strings.has_prefix(src, "[]") {
elem := strings.trim_space(src[2:])
et, ok := resolve_type(g, elem, line)
if !ok {
return {}, false
}
if et.of == .Array {
errorf(g, line, "nested slice '%s' is not supported; declare it as a jobject instead", src)
return {}, false
}
return Type_Ref{
odin = src, sig = fmt.aprintf("[%s", et.sig), of = .Array,
call = "object", jval = "l", elem = elem,
}, true
}
// A raw jobject is untyped on the Java side, so it signs as Object.
if src == "android.jobject" || src == "jobject" {
return Type_Ref{
odin = src, sig = "Ljava/lang/Object;", of = .Object,
call = "object", jval = "l",
}, true
}
if cls, ok := g.classes[src]; ok {
return Type_Ref{
odin = src, sig = fmt.aprintf("L%s;", cls), of = .Object,
call = "object", jval = "l",
}, true
}
errorf(g, line, "unknown type '%s'; register it with @(java_class=\"...\") or use a primitive", src)
return {}, false
}
// Whether a returned value allocates and so needs an allocator parameter.
allocates :: proc(t: Type_Ref) -> bool {
if t.of == .String {
return true
}
return t.of == .Array
}
+146
View File
@@ -0,0 +1,146 @@
package main
import "shared:android"
import "core:fmt"
import "base:runtime"
import engine "../zephr"
import "jni"
counter := 0
device_model: string
device_sdk: i32
toast_ctx: Context
main_activity: Activity
Edge_Insets :: struct {
left, top, right, bottom: f32
}
insets: Edge_Insets
insets_valid: bool
@export
android_main :: proc "c" (state: ^android.android_app) {
context = runtime.default_context()
context.user_ptr = state
engine.init("", "", {1000, 600}, false, "")
defer engine.deinit()
// Static field reads only -- safe from the engine's thread. Anything
// touching the UI toolkit (Toast, dialogs) needs the main looper instead.
if jni.init(state) {
device_model, _ = build_model()
device_sdk, _ = build_sdk_int()
toast_ctx = Context(jni.activity(state))
main_activity = Activity(jni.activity(state))
}
_, font_ok := engine.ui_text_register_font(#load("/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf"))
ensure(font_ok)
for !engine.should_quit() {
engine.frame_start()
defer engine.frame_end()
e := engine.iter_events()
for e != nil {
#partial switch e.type {
case .WINDOW_CLOSED:
engine.quit()
case .WINDOW_RESIZED:
insets_valid = false
}
e = engine.iter_events()
}
if !insets_valid {
insets_valid = refresh_insets()
}
window_size := engine.get_window_size()
engine.ui_begin_build(window_size)
engine.ui_text_set_default_pixel_size(108)
engine.ui_push_text_color(0xFFFFFF)
draw_ui()
engine.ui_pop_text_color()
engine.ui_end_build()
engine.ui_draw(engine.get_projection())
}
}
draw_ui :: proc() {
engine.ui_set_next_width(engine.ui_children_sum(1))
engine.ui_column(); {
engine.ui_padding(insets.left, {.Left})
engine.ui_padding(insets.right, {.Right})
engine.ui_padding(insets.top, {.Top})
engine.ui_padding(insets.bottom, {.Bottom})
engine.ui_text(fmt.tprintf("%d", counter))
engine.ui_text(fmt.tprintf("%s / API %d", device_model, device_sdk))
engine.ui_text(fmt.tprintf("ins %v %v %v %v ok=%v", insets.left, insets.top, insets.right, insets.bottom, insets_valid))
if click, _ := engine.ui_button("Hello"); click {
counter += 1
show_toast(fmt.tprintf("Clicked %d times", counter))
}
}
}
// Toast needs a Java looper on this thread. Preparing one is a one-time cost
// and the native ALooper the engine polls is unaffected -- they're separate.
show_toast :: proc(msg: string) {
if l, ok := looper_my_looper(); ok && l == nil {
looper_prepare()
}
toast, ok := toast_make_text(toast_ctx, msg, 0) // 0 = Toast.LENGTH_SHORT
if !ok {
return
}
toast_show(toast)
}
// Walks Activity -> Window -> decor View -> WindowInsets. Returns false while
// the decor view is still unattached, which is normal for the first few frames.
refresh_insets :: proc() -> bool {
// This runs every frame until it succeeds, and each call below returns a
// local ref. Without a frame to scope them the ref table fills and aborts.
env := jni.env()
if !jni.push_frame(env) {
return false
}
defer jni.pop_frame(env)
win := activity_get_window(main_activity) or_return
decor := window_get_decor_view(win) or_return
wi := view_get_root_window_insets(decor) or_return
if wi == nil {
return false
}
// API 30+ typed insets. Below that both calls fail and we fall through.
if mask, ok := insets_type_system_bars(); ok {
if cut, got_cut := insets_type_display_cutout(); got_cut {
mask |= cut
}
if ins, got := window_insets_get_ignoring_visibility(wi, mask); got && ins != nil {
l := insets_left(ins) or_return
t := insets_top(ins) or_return
r := insets_right(ins) or_return
b := insets_bottom(ins) or_return
insets = {f32(l), f32(t), f32(r), f32(b)}
return true
}
}
l := window_insets_sys_left(wi) or_return
t := window_insets_sys_top(wi) or_return
r := window_insets_sys_right(wi) or_return
b := window_insets_sys_bottom(wi) or_return
insets = {f32(l), f32(t), f32(r), f32(b)}
return true
}