From a9a97a958c81a74c67d5d8f47a7ae53be07c6aa3 Mon Sep 17 00:00:00 2001 From: Greg Date: Fri, 19 Jun 2020 16:07:37 -0400 Subject: [PATCH] Initial commit. --- LICENSE | 27 ++++++ gojni.h | 25 ++++++ jni.c | 101 ++++++++++++++++++++++ jni.go | 261 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 414 insertions(+) create mode 100644 LICENSE create mode 100644 gojni.h create mode 100644 jni.c create mode 100644 jni.go diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..eb7c3f6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2020 Tailscale & AUTHORS. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Tailscale Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/gojni.h b/gojni.h new file mode 100644 index 0000000..6895305 --- /dev/null +++ b/gojni.h @@ -0,0 +1,25 @@ +__attribute__ ((visibility ("hidden"))) jint _jni_GetEnv(JavaVM *vm, JNIEnv **env, jint version); +__attribute__ ((visibility ("hidden"))) jint _jni_AttachCurrentThread(JavaVM *vm, JNIEnv **p_env, void *thr_args); +__attribute__ ((visibility ("hidden"))) jint _jni_DetachCurrentThread(JavaVM *vm); +__attribute__ ((visibility ("hidden"))) jclass _jni_FindClass(JNIEnv *env, const char *name); +__attribute__ ((visibility ("hidden"))) jthrowable _jni_ExceptionOccurred(JNIEnv *env); +__attribute__ ((visibility ("hidden"))) void _jni_ExceptionClear(JNIEnv *env); +__attribute__ ((visibility ("hidden"))) jclass _jni_GetObjectClass(JNIEnv *env, jobject obj); +__attribute__ ((visibility ("hidden"))) jmethodID _jni_GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig); +__attribute__ ((visibility ("hidden"))) jmethodID _jni_GetStaticMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig); +__attribute__ ((visibility ("hidden"))) jsize _jni_GetStringLength(JNIEnv *env, jstring str); +__attribute__ ((visibility ("hidden"))) const jchar *_jni_GetStringChars(JNIEnv *env, jstring str); +__attribute__ ((visibility ("hidden"))) jstring _jni_NewString(JNIEnv *env, const jchar *unicodeChars, jsize len); +__attribute__ ((visibility ("hidden"))) jboolean _jni_IsSameObject(JNIEnv *env, jobject ref1, jobject ref2); +__attribute__ ((visibility ("hidden"))) jobject _jni_NewGlobalRef(JNIEnv *env, jobject obj); +__attribute__ ((visibility ("hidden"))) void _jni_DeleteGlobalRef(JNIEnv *env, jobject obj); +__attribute__ ((visibility ("hidden"))) jint _jni_CallStaticIntMethodA(JNIEnv *env, jclass cls, jmethodID method, jvalue *args); +__attribute__ ((visibility ("hidden"))) jobject _jni_CallStaticObjectMethodA(JNIEnv *env, jclass cls, jmethodID method, jvalue *args); +__attribute__ ((visibility ("hidden"))) void _jni_CallStaticVoidMethodA(JNIEnv *env, jclass cls, jmethodID method, jvalue *args); +__attribute__ ((visibility ("hidden"))) jobject _jni_CallObjectMethodA(JNIEnv *env, jobject obj, jmethodID method, jvalue *args); +__attribute__ ((visibility ("hidden"))) jint _jni_CallIntMethodA(JNIEnv *env, jobject obj, jmethodID method, jvalue *args); +__attribute__ ((visibility ("hidden"))) void _jni_CallVoidMethodA(JNIEnv *env, jobject obj, jmethodID method, jvalue *args); +__attribute__ ((visibility ("hidden"))) jbyteArray _jni_NewByteArray(JNIEnv *env, jsize length); +__attribute__ ((visibility ("hidden"))) jbyte *_jni_GetByteArrayElements(JNIEnv *env, jbyteArray arr); +__attribute__ ((visibility ("hidden"))) void _jni_ReleaseByteArrayElements(JNIEnv *env, jbyteArray arr, jbyte *elems, jint mode); +__attribute__ ((visibility ("hidden"))) jsize _jni_GetArrayLength(JNIEnv *env, jarray arr); diff --git a/jni.c b/jni.c new file mode 100644 index 0000000..45b1d27 --- /dev/null +++ b/jni.c @@ -0,0 +1,101 @@ +#include + +jint _jni_AttachCurrentThread(JavaVM *vm, JNIEnv **p_env, void *thr_args) { + return (*vm)->AttachCurrentThread(vm, p_env, thr_args); +} + +jint _jni_DetachCurrentThread(JavaVM *vm) { + return (*vm)->DetachCurrentThread(vm); +} + +jint _jni_GetEnv(JavaVM *vm, JNIEnv **env, jint version) { + return (*vm)->GetEnv(vm, (void **)env, version); +} + +jclass _jni_FindClass(JNIEnv *env, const char *name) { + return (*env)->FindClass(env, name); +} + +jthrowable _jni_ExceptionOccurred(JNIEnv *env) { + return (*env)->ExceptionOccurred(env); +} + +void _jni_ExceptionClear(JNIEnv *env) { + (*env)->ExceptionClear(env); +} + +jclass _jni_GetObjectClass(JNIEnv *env, jobject obj) { + return (*env)->GetObjectClass(env, obj); +} + +jmethodID _jni_GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig) { + return (*env)->GetMethodID(env, clazz, name, sig); +} + +jmethodID _jni_GetStaticMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig) { + return (*env)->GetStaticMethodID(env, clazz, name, sig); +} + +jsize _jni_GetStringLength(JNIEnv *env, jstring str) { + return (*env)->GetStringLength(env, str); +} + +const jchar *_jni_GetStringChars(JNIEnv *env, jstring str) { + return (*env)->GetStringChars(env, str, NULL); +} + +jstring _jni_NewString(JNIEnv *env, const jchar *unicodeChars, jsize len) { + return (*env)->NewString(env, unicodeChars, len); +} + +jboolean _jni_IsSameObject(JNIEnv *env, jobject ref1, jobject ref2) { + return (*env)->IsSameObject(env, ref1, ref2); +} + +jobject _jni_NewGlobalRef(JNIEnv *env, jobject obj) { + return (*env)->NewGlobalRef(env, obj); +} + +void _jni_DeleteGlobalRef(JNIEnv *env, jobject obj) { + (*env)->DeleteGlobalRef(env, obj); +} + +void _jni_CallStaticVoidMethodA(JNIEnv *env, jclass cls, jmethodID method, jvalue *args) { + (*env)->CallStaticVoidMethodA(env, cls, method, args); +} + +jint _jni_CallStaticIntMethodA(JNIEnv *env, jclass cls, jmethodID method, jvalue *args) { + return (*env)->CallStaticIntMethodA(env, cls, method, args); +} + +jobject _jni_CallStaticObjectMethodA(JNIEnv *env, jclass cls, jmethodID method, jvalue *args) { + return (*env)->CallStaticObjectMethodA(env, cls, method, args); +} + +jobject _jni_CallObjectMethodA(JNIEnv *env, jobject obj, jmethodID method, jvalue *args) { + return (*env)->CallObjectMethodA(env, obj, method, args); +} + +jint _jni_CallIntMethodA(JNIEnv *env, jobject obj, jmethodID method, jvalue *args) { + return (*env)->CallIntMethodA(env, obj, method, args); +} + +void _jni_CallVoidMethodA(JNIEnv *env, jobject obj, jmethodID method, jvalue *args) { + (*env)->CallVoidMethodA(env, obj, method, args); +} + +jbyteArray _jni_NewByteArray(JNIEnv *env, jsize length) { + return (*env)->NewByteArray(env, length); +} + +jbyte *_jni_GetByteArrayElements(JNIEnv *env, jbyteArray arr) { + return (*env)->GetByteArrayElements(env, arr, NULL); +} + +void _jni_ReleaseByteArrayElements(JNIEnv *env, jbyteArray arr, jbyte *elems, jint mode) { + (*env)->ReleaseByteArrayElements(env, arr, elems, mode); +} + +jsize _jni_GetArrayLength(JNIEnv *env, jarray arr) { + return (*env)->GetArrayLength(env, arr); +} diff --git a/jni.go b/jni.go new file mode 100644 index 0000000..b3485d7 --- /dev/null +++ b/jni.go @@ -0,0 +1,261 @@ +// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package jni + +// Package jni implements various helper functions for communicating with the Android JVM +// though JNI. + +import ( + "errors" + "fmt" + "reflect" + "runtime" + "unicode/utf16" + "unsafe" +) + +/* +#cgo CFLAGS: -Wall + +#include +#include + +#include "gojni.h" +*/ +import "C" + +type JVM struct { + jvm *C.JavaVM +} + +type Env struct { + env *C.JNIEnv +} + +type ( + Class C.jclass + Object C.jobject + MethodID C.jmethodID + String C.jstring + ByteArray C.jbyteArray + Value uint64 // All JNI types fit into 64-bits. +) + +func JVMFor(jvmPtr uintptr) JVM { + return JVM{ + jvm: (*C.JavaVM)(unsafe.Pointer(jvmPtr)), + } +} + +func EnvFor(envPtr uintptr) Env { + return Env{ + env: (*C.JNIEnv)(unsafe.Pointer(envPtr)), + } +} + +// Do invokes a function with a temporary JVM environment. The +// environment is not valid after the function returns. +func Do(vm JVM, f func(env Env) error) error { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + var env *C.JNIEnv + if res := C._jni_GetEnv(vm.jvm, &env, C.JNI_VERSION_1_6); res != C.JNI_OK { + if res != C.JNI_EDETACHED { + panic(fmt.Errorf("JNI GetEnv failed with error %d", res)) + } + if C._jni_AttachCurrentThread(vm.jvm, &env, nil) != C.JNI_OK { + panic(errors.New("runInJVM: AttachCurrentThread failed")) + } + defer C._jni_DetachCurrentThread(vm.jvm) + } + + return f(Env{env}) +} + +func varArgs(args []Value) *C.jvalue { + if len(args) == 0 { + return nil + } + return (*C.jvalue)(unsafe.Pointer(&args[0])) +} + +func IsSameObject(e Env, ref1, ref2 Object) bool { + same := C._jni_IsSameObject(e.env, C.jobject(ref1), C.jobject(ref2)) + return same == C.JNI_TRUE +} + +func CallStaticIntMethod(e Env, cls Class, method MethodID, args ...Value) (int, error) { + res := C._jni_CallStaticIntMethodA(e.env, C.jclass(cls), C.jmethodID(method), varArgs(args)) + return int(res), exception(e) +} + +func CallStaticVoidMethod(e Env, cls Class, method MethodID, args ...Value) error { + C._jni_CallStaticVoidMethodA(e.env, C.jclass(cls), C.jmethodID(method), varArgs(args)) + return exception(e) +} + +func CallVoidMethod(e Env, obj Object, method MethodID, args ...Value) error { + C._jni_CallVoidMethodA(e.env, C.jobject(obj), C.jmethodID(method), varArgs(args)) + return exception(e) +} + +func CallStaticObjectMethod(e Env, cls Class, method MethodID, args ...Value) (Object, error) { + res := C._jni_CallStaticObjectMethodA(e.env, C.jclass(cls), C.jmethodID(method), varArgs(args)) + return Object(res), exception(e) +} + +func CallObjectMethod(e Env, obj Object, method MethodID, args ...Value) (Object, error) { + res := C._jni_CallObjectMethodA(e.env, C.jobject(obj), C.jmethodID(method), varArgs(args)) + return Object(res), exception(e) +} + +func CallIntMethod(e Env, obj Object, method MethodID, args ...Value) (int32, error) { + res := C._jni_CallIntMethodA(e.env, C.jobject(obj), C.jmethodID(method), varArgs(args)) + return int32(res), exception(e) +} + +// GetByteArrayElements returns the contents of the array. +func GetByteArrayElements(e Env, jarr ByteArray) []byte { + size := C._jni_GetArrayLength(e.env, C.jarray(jarr)) + elems := C._jni_GetByteArrayElements(e.env, C.jbyteArray(jarr)) + defer C._jni_ReleaseByteArrayElements(e.env, C.jbyteArray(jarr), elems, 0) + backing := (*(*[1 << 30]byte)(unsafe.Pointer(elems)))[:size:size] + s := make([]byte, len(backing)) + copy(s, backing) + return s +} + +// NewByteArray allocates a Java byte array with the content. It +// panics if the allocation fails. +func NewByteArray(e Env, content []byte) ByteArray { + jarr := C._jni_NewByteArray(e.env, C.jsize(len(content))) + if jarr == 0 { + panic(fmt.Errorf("jni: NewByteArray(%d) failed", len(content))) + } + elems := C._jni_GetByteArrayElements(e.env, jarr) + defer C._jni_ReleaseByteArrayElements(e.env, jarr, elems, 0) + backing := (*(*[1 << 30]byte)(unsafe.Pointer(elems)))[:len(content):len(content)] + copy(backing, content) + return ByteArray(jarr) +} + +// ClassLoader returns a reference to the Java ClassLoader associated +// with obj. +func ClassLoaderFor(e Env, obj Object) Object { + cls := GetObjectClass(e, obj) + getClassLoader := GetMethodID(e, cls, "getClassLoader", "()Ljava/lang/ClassLoader;") + clsLoader, err := CallObjectMethod(e, Object(obj), getClassLoader) + if err != nil { + // Class.getClassLoader should never fail. + panic(err) + } + return Object(clsLoader) +} + +// LoadClass invokes the underlying ClassLoader's loadClass method and +// returns the class. +func LoadClass(e Env, loader Object, class string) (Class, error) { + cls := GetObjectClass(e, loader) + loadClass := GetMethodID(e, cls, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;") + name := JavaString(e, class) + loaded, err := CallObjectMethod(e, loader, loadClass, Value(name)) + if err != nil { + return 0, err + } + return Class(loaded), exception(e) +} + +// exception returns an error corresponding to the pending +// exception, and clears it. exceptionError returns nil if no +// exception is pending. +func exception(e Env) error { + thr := C._jni_ExceptionOccurred(e.env) + if thr == 0 { + return nil + } + C._jni_ExceptionClear(e.env) + cls := GetObjectClass(e, Object(thr)) + toString := GetMethodID(e, cls, "toString", "()Ljava/lang/String;") + msg, err := CallObjectMethod(e, Object(thr), toString) + if err != nil { + return err + } + return errors.New(GoString(e, String(msg))) +} + +// GetObjectClass returns the Java Class for an Object. +func GetObjectClass(e Env, obj Object) Class { + if obj == 0 { + panic("null object") + } + cls := C._jni_GetObjectClass(e.env, C.jobject(obj)) + if err := exception(e); err != nil { + // GetObjectClass should never fail. + panic(err) + } + return Class(cls) +} + +// GetStaticMethodID returns the id for a static method. It panics if the method +// wasn't found. +func GetStaticMethodID(e Env, cls Class, name, signature string) MethodID { + mname := C.CString(name) + defer C.free(unsafe.Pointer(mname)) + msig := C.CString(signature) + defer C.free(unsafe.Pointer(msig)) + m := C._jni_GetStaticMethodID(e.env, C.jclass(cls), mname, msig) + if err := exception(e); err != nil { + panic(err) + } + return MethodID(m) +} + +// GetMethodID returns the id for a method. It panics if the method +// wasn't found. +func GetMethodID(e Env, cls Class, name, signature string) MethodID { + mname := C.CString(name) + defer C.free(unsafe.Pointer(mname)) + msig := C.CString(signature) + defer C.free(unsafe.Pointer(msig)) + m := C._jni_GetMethodID(e.env, C.jclass(cls), mname, msig) + if err := exception(e); err != nil { + panic(err) + } + return MethodID(m) +} + +func NewGlobalRef(e Env, obj Object) Object { + return Object(C._jni_NewGlobalRef(e.env, C.jobject(obj))) +} + +func DeleteGlobalRef(e Env, obj Object) { + C._jni_DeleteGlobalRef(e.env, C.jobject(obj)) +} + +// JavaString converts the string to a JVM jstring. +func JavaString(e Env, str string) String { + if str == "" { + return 0 + } + utf16Chars := utf16.Encode([]rune(str)) + res := C._jni_NewString(e.env, (*C.jchar)(unsafe.Pointer(&utf16Chars[0])), C.int(len(utf16Chars))) + return String(res) +} + +// GoString converts the JVM jstring to a Go string. +func GoString(e Env, str String) string { + if str == 0 { + return "" + } + strlen := C._jni_GetStringLength(e.env, C.jstring(str)) + chars := C._jni_GetStringChars(e.env, C.jstring(str)) + var utf16Chars []uint16 + hdr := (*reflect.SliceHeader)(unsafe.Pointer(&utf16Chars)) + hdr.Data = uintptr(unsafe.Pointer(chars)) + hdr.Cap = int(strlen) + hdr.Len = int(strlen) + utf8 := utf16.Decode(utf16Chars) + return string(utf8) +}