From f6141fd410855a82b9bbaa02a0b2634ed137843c Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Wed, 14 Aug 2019 16:54:11 +0200 Subject: rename library->akono --- .gitignore | 4 +- akono/build.gradle.kts | 79 +++ .../java/akono/InstrumentedAkonoTests.kt | 85 +++ akono/src/main/AndroidManifest.xml | 7 + akono/src/main/cpp/CMakeLists.txt | 42 ++ akono/src/main/cpp/akono-jni.cpp | 588 +++++++++++++++++++++ akono/src/main/java/akono/AkonoJni.kt | 208 ++++++++ akono/src/main/java/akono/Library.kt | 10 + akono/src/test/java/akono/LibraryTest.kt | 16 + library/build.gradle.kts | 79 --- .../java/akono/InstrumentedAkonoTests.kt | 85 --- library/src/main/AndroidManifest.xml | 7 - library/src/main/cpp/CMakeLists.txt | 42 -- library/src/main/cpp/akono-jni.cpp | 588 --------------------- library/src/main/java/akono/AkonoJni.kt | 208 -------- library/src/main/java/akono/Library.kt | 10 - library/src/test/java/akono/LibraryTest.kt | 16 - settings.gradle.kts | 2 +- 18 files changed, 1038 insertions(+), 1038 deletions(-) create mode 100644 akono/build.gradle.kts create mode 100644 akono/src/androidTest/java/akono/InstrumentedAkonoTests.kt create mode 100644 akono/src/main/AndroidManifest.xml create mode 100644 akono/src/main/cpp/CMakeLists.txt create mode 100644 akono/src/main/cpp/akono-jni.cpp create mode 100644 akono/src/main/java/akono/AkonoJni.kt create mode 100644 akono/src/main/java/akono/Library.kt create mode 100644 akono/src/test/java/akono/LibraryTest.kt delete mode 100644 library/build.gradle.kts delete mode 100644 library/src/androidTest/java/akono/InstrumentedAkonoTests.kt delete mode 100644 library/src/main/AndroidManifest.xml delete mode 100644 library/src/main/cpp/CMakeLists.txt delete mode 100644 library/src/main/cpp/akono-jni.cpp delete mode 100644 library/src/main/java/akono/AkonoJni.kt delete mode 100644 library/src/main/java/akono/Library.kt delete mode 100644 library/src/test/java/akono/LibraryTest.kt diff --git a/.gitignore b/.gitignore index aca45b18..ee8d3764 100644 --- a/.gitignore +++ b/.gitignore @@ -3,14 +3,14 @@ # Ignore Gradle build output directory /build -/library/build +/akono/build .externalNativeBuild # Ignore Android Studio project file *.iml # Ignore Android generated files -/library/src/main/gen/ +/akono/src/main/gen/ # https://intellij-support.jetbrains.com/hc/en-us/articles/206544839-How-to-manage-projects-under-Version-Control-Systems /.idea/modules.xml diff --git a/akono/build.gradle.kts b/akono/build.gradle.kts new file mode 100644 index 00000000..b98510db --- /dev/null +++ b/akono/build.gradle.kts @@ -0,0 +1,79 @@ + +plugins { + id("com.android.library") + kotlin("android") + kotlin("android.extensions") +} + +android { + compileSdkVersion(28) + defaultConfig { + minSdkVersion(26) + targetSdkVersion(28) + + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + // Specifies the application ID for the test APK. + testApplicationId = "akono.test" + + ndk { + // Tells Gradle to build outputs for the following ABIs and package + // them into your APK. + abiFilters("armeabi-v7a"); + } + + externalNativeBuild { + cmake.arguments("-DANDROID_STL=c++_shared") + } + } + useLibrary("android.test.runner") + useLibrary("android.test.base") + useLibrary("android.test.mock") + + externalNativeBuild { + cmake { + setPath(file("src/main/cpp/CMakeLists.txt")) + } + } + + sourceSets { + named("main") { + jniLibs.srcDirs("../deps/compiled") + } + } +} + +val kotlin_version: String by rootProject.extra + +repositories { + jcenter() +} + +dependencies { + //implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.3.20") + //implementation(kotlin("stdlib")) + + // Use the Kotlin test library. + testImplementation("org.jetbrains.kotlin:kotlin-test:$kotlin_version") + + // Use the Kotlin JUnit integration. + testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version") + + androidTestImplementation("androidx.test:core:1.1.0") + androidTestImplementation("androidx.test:runner:1.1.1") + androidTestImplementation("androidx.test:rules:1.1.1") + + // Assertions + androidTestImplementation("androidx.test.ext:junit:1.1.0") + androidTestImplementation("androidx.test.ext:truth:1.1.0") + androidTestImplementation("com.google.truth:truth:0.44") + + // Use the Kotlin test library. + androidTestImplementation("org.jetbrains.kotlin:kotlin-test:$kotlin_version") + + // Use the Kotlin JUnit integration. + androidTestImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version") + implementation(kotlin("stdlib-jdk7", kotlin_version)) +} diff --git a/akono/src/androidTest/java/akono/InstrumentedAkonoTests.kt b/akono/src/androidTest/java/akono/InstrumentedAkonoTests.kt new file mode 100644 index 00000000..beda5119 --- /dev/null +++ b/akono/src/androidTest/java/akono/InstrumentedAkonoTests.kt @@ -0,0 +1,85 @@ +package akono.test; + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.runner.RunWith +import org.junit.Test +import androidx.test.filters.LargeTest +import org.junit.Assert.assertEquals +import akono.AkonoJni +import akono.ModuleResult +import android.util.Log +import java.util.concurrent.LinkedBlockingDeque + + +class SyncMessageHandler : AkonoJni.MessageHandler { + private val messageQueue = LinkedBlockingDeque() + override fun handleMessage(message: String) { + messageQueue.put(message) + } + + fun waitForMessage(): String { + return messageQueue.take() + } +} + + +class StaticModuleLoadHandler : AkonoJni.LoadModuleHandler { + private val modules: MutableMap = HashMap() + + override fun loadModule(name: String, paths: Array): ModuleResult? { + val code = modules.get(name) ?: return null + if (modules.containsKey(name)) { + return ModuleResult("/vmodroot/$name.js", code) + } + return null + } + + fun registerModule(name: String, source: String) { + modules[name] = source + } +} + + +// @RunWith is required only if you use a mix of JUnit3 and JUnit4. +@RunWith(AndroidJUnit4::class) +@LargeTest +public class InstrumentedAkonoTestOne { + @Test + fun myJsTest() { + val ajni: AkonoJni = AkonoJni() + assertEquals("2", ajni.evalSimpleJs("1+1")) + assertEquals("36", ajni.evalSimpleJs("6*6")) + assertEquals("42", ajni.evalSimpleJs("(()=>{let x = 42; return x;})()")) + assertEquals("undefined", ajni.evalSimpleJs("const myVal = 42")) + assertEquals("43", ajni.evalSimpleJs("myVal + 1")) + + val myHandler = SyncMessageHandler() + ajni.setMessageHandler(myHandler) + ajni.evalNodeCode("console.log('hi from the test case')") + // Tell the message handler to just ping back messages to us + ajni.evalNodeCode("global.__akono_onMessage = (x) => { global.__akono_sendMessage(x); }") + val sentMessage = "Hello AKONO!!" + ajni.sendMessage(sentMessage) + val receivedMessage = myHandler.waitForMessage() + assertEquals(sentMessage, receivedMessage) + Log.i("myapp", "test case received message: $receivedMessage") + + val myModHandler = StaticModuleLoadHandler() + + ajni.setLoadModuleHandler(myModHandler) + + myModHandler.registerModule("a", """ + |console.log('I am module a'); + |exports.foo = () => { global.__akono_sendMessage('hello42'); }; + """.trimMargin()) + + ajni.evalNodeCode("a = require('a');") + ajni.evalNodeCode("a.foo()") + + val msg2 = myHandler.waitForMessage() + + assertEquals("hello42", msg2) + + ajni.waitStopped() + } +} diff --git a/akono/src/main/AndroidManifest.xml b/akono/src/main/AndroidManifest.xml new file mode 100644 index 00000000..3a8d4915 --- /dev/null +++ b/akono/src/main/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/akono/src/main/cpp/CMakeLists.txt b/akono/src/main/cpp/CMakeLists.txt new file mode 100644 index 00000000..10d6396f --- /dev/null +++ b/akono/src/main/cpp/CMakeLists.txt @@ -0,0 +1,42 @@ +cmake_minimum_required(VERSION 3.4.1) + +add_library( + akono-jni SHARED + akono-jni.cpp +) + +set(deps_dir ${CMAKE_CURRENT_SOURCE_DIR}/../../../../deps) + +if(NOT EXISTS ${deps_dir}) + message( FATAL_ERROR "Dependency directory does not exist") +endif() + +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++14") + +include_directories( + ${deps_dir}/android-node-v8/src + ${deps_dir}/android-node-v8/deps/v8/include + ${deps_dir}/android-node-v8/deps/uv/include +) + +add_library(node SHARED IMPORTED) +set_target_properties(node PROPERTIES IMPORTED_LOCATION + ${deps_dir}/compiled/${ANDROID_ABI}/libnode.so) + +add_library(v8 STATIC IMPORTED) +set_target_properties(v8 PROPERTIES IMPORTED_LOCATION + ${deps_dir}/compiled/${ANDROID_ABI}/libv8.cr.so) + +add_library(v8_platform STATIC IMPORTED) +set_target_properties(v8_platform PROPERTIES IMPORTED_LOCATION + ${deps_dir}/compiled/${ANDROID_ABI}/libv8_libplatform.cr.so) + +# Include libraries needed for hello-jni lib +target_link_libraries(akono-jni + v8 + v8_platform + node + android + log) + + diff --git a/akono/src/main/cpp/akono-jni.cpp b/akono/src/main/cpp/akono-jni.cpp new file mode 100644 index 00000000..b9e71012 --- /dev/null +++ b/akono/src/main/cpp/akono-jni.cpp @@ -0,0 +1,588 @@ +#include +#include +#include +#include + +#define NODE_WANT_INTERNALS 1 + +#include + +#include +#include +#include +#include +#include + + + +// Provide stubs so that libnode.so links properly +namespace node { + namespace native_module { + const bool has_code_cache = false; + + void NativeModuleEnv::InitializeCodeCache() {} + } + + v8::StartupData *NodeMainInstance::GetEmbeddedSnapshotBlob() { + return nullptr; + } + + const std::vector *NodeMainInstance::GetIsolateDataIndexes() { + return nullptr; + } +} + + +static int pfd[2]; +static pthread_t thr; +static const char *tag = "myapp"; + + +static void *thread_func(void *) { + ssize_t rdsz; + char buf[128]; + while ((rdsz = read(pfd[0], buf, sizeof buf - 1)) > 0) { + if (buf[rdsz - 1] == '\n') --rdsz; + buf[rdsz] = 0; /* add null-terminator */ + __android_log_write(ANDROID_LOG_DEBUG, tag, buf); + } + return 0; +} + +static void mylog(const char *msg) { + __android_log_write(ANDROID_LOG_DEBUG, tag, msg); +} + +int start_logger(const char *app_name) { + tag = app_name; + + /* make stdout line-buffered and stderr unbuffered */ + setvbuf(stdout, 0, _IOLBF, 0); + setvbuf(stderr, 0, _IONBF, 0); + + /* create the pipe and redirect stdout and stderr */ + pipe(pfd); + dup2(pfd[1], 1); + dup2(pfd[1], 2); + + /* spawn the logging thread */ + if (pthread_create(&thr, 0, thread_func, 0) == -1) + return -1; + pthread_detach(thr); + return 0; +} + + +/** + * Helper class to manage conversion from a JNI string to a C string. + */ +class JStringValue { +private: + jstring m_jstr; + const char *m_cstr; + JNIEnv *m_env; +public: + JStringValue(JNIEnv *env, jstring s) : m_env(env), m_jstr(s) { + m_cstr = env->GetStringUTFChars(s, NULL); + } + + ~JStringValue() { + m_env->ReleaseStringUTFChars(m_jstr, m_cstr); + } + + const char *operator*() { + return m_cstr; + } +}; + + +/** + * Slightly more sane wrapper around node::Init + */ +static void InitNode(std::vector argv) { + int ret_exec_argc = 0; + int ret_argc = argv.size(); + const char **ret_exec_argv = nullptr; + + node::Init(&ret_argc, &argv[0], &ret_exec_argc, &ret_exec_argv); +} + +// Forward declarations +void notifyCb(uv_async_t *async); + +static void sendMessageCallback(const v8::FunctionCallbackInfo &args); +static void loadModuleCallback(const v8::FunctionCallbackInfo &args); +static void getDataCallback(const v8::FunctionCallbackInfo &args); + +static const char *main_code = "global.__akono_run = (x) => {" + " console.log('running code', x);" + " global.eval(x);" + "};" + "" + "global.__akono_onMessage = (x) => {" + " console.log('got __akono_onMessage', x);" + "};" + "" + "mod = require('module');" + "mod._saved_findPath = mod._findPath;" + "mod._akonoMods = {};" + "mod._findPath = (request, paths, isMain) => {" + " console.log('in _findPath');" + " const res = mod._saved_findPath(request, paths, isMain);" + " if (res !== false) return res;" + " const args = JSON.stringify({ request, paths});" + " const loadResult = JSON.parse(global.__akono_loadModule(args));" + " console.log('got loadModule result', loadResult);" + " if (!loadResult) return false;" + " mod._akonoMods[loadResult.path] = loadResult;" + " console.log('returning path', loadResult.path);" + " return loadResult.path;" + "};" + "" + "function stripBOM(content) {" + " if (content.charCodeAt(0) === 0xFEFF) {" + " content = content.slice(1);" + " }" + " return content;" + "}" + "" + "mod._saved_js_extension = mod._extensions[\".js\"];" + "mod._extensions[\".js\"] = (module, filename) => {" + " console.log('handling js extension', [module, filename]);" + " if (mod._akonoMods.hasOwnProperty(filename)) {" + " const akmod = mod._akonoMods[filename];" + " console.log('found mod', akmod);" + " const content = akmod.content;" + " return module._compile(stripBOM(content), filename);" + " }" + " console.log('falling back');" + " return mod._saved_js_extension(module, filename);" + "};"; + + +class NativeAkonoInstance { +private: + static bool logInitialized; + static bool v8Initialized; + //static std::unique_ptr platform; + static node::MultiIsolatePlatform *platform; +public: + v8::Isolate *isolate; + node::Environment *environment; + v8::Persistent globalContext; + uv_async_t async_notify; + uv_loop_t *loop; + bool breakRequested = false; + JNIEnv *currentJniEnv = nullptr; + jobject currentJniThiz = nullptr; + + NativeAkonoInstance() : globalContext() { + loop = uv_default_loop(); + uv_async_init(loop, &async_notify, notifyCb); + async_notify.data = this; + + if (!logInitialized) { + start_logger("myapp"); + logInitialized = true; + } + + if (!v8Initialized) { + //platform = v8::platform::NewDefaultPlatform(); + //v8::V8::InitializePlatform(platform.get()); + //v8::V8::Initialize(); + + // Here, only the arguments used to initialize the global node/v8 platform + // are relevant, the others are skipped. + InitNode(std::vector{"node", "-e", main_code}); + platform = node::InitializeV8Platform(10); + v8::V8::Initialize(); + + v8Initialized = true; + } + + node::ArrayBufferAllocator *allocator = node::CreateArrayBufferAllocator(); + this->isolate = node::NewIsolate(allocator, uv_default_loop()); + + + v8::Isolate::Scope isolate_scope(isolate); + v8::HandleScope handle_scope(isolate); + + node::IsolateData *isolateData = node::CreateIsolateData( + this->isolate, + uv_default_loop(), + platform, + allocator); + + + globalContext.Reset(isolate, node::NewContext(isolate)); + + v8::Local context = globalContext.Get(isolate); + + // Arguments for node itself + std::vector nodeArgv{"node", "-e", "console.log('hello world');"}; + // Arguments for the script run by node + std::vector nodeExecArgv{}; + + mylog("entering global scopt"); + + v8::Context::Scope context_scope(globalContext.Get(isolate)); + + mylog("creating environment"); + + node::Environment *environment = node::CreateEnvironment( + isolateData, + globalContext.Get(isolate), + nodeArgv.size(), + &nodeArgv[0], + nodeExecArgv.size(), + &nodeExecArgv[0]); + + + mylog("loading environment"); + + node::LoadEnvironment(environment); + + mylog("finished loading environment"); + + v8::Local dataTemplate = v8::ObjectTemplate::New(isolate); + dataTemplate->SetInternalFieldCount(1); + v8::Local dataObject = dataTemplate->NewInstance(context).ToLocalChecked(); + dataObject->SetAlignedPointerInInternalField(0, this); + + v8::Local sendMessageFunction = v8::Function::New(context, + sendMessageCallback, + dataObject).ToLocalChecked(); + + v8::Local loadModuleFunction = v8::Function::New(context, + loadModuleCallback, + dataObject).ToLocalChecked(); + + v8::Local getDataFunction = v8::Function::New(context, + getDataCallback, + dataObject).ToLocalChecked(); + + v8::Local global = context->Global(); + + global->Set(v8::String::NewFromUtf8(isolate, "__akono_sendMessage", + v8::NewStringType::kNormal).ToLocalChecked(), + sendMessageFunction); + + global->Set(v8::String::NewFromUtf8(isolate, "__akono_loadModule", + v8::NewStringType::kNormal).ToLocalChecked(), + loadModuleFunction); + + // Get data synchronously (!) from the embedder + global->Set(v8::String::NewFromUtf8(isolate, "__akono_getData", + v8::NewStringType::kNormal).ToLocalChecked(), + getDataFunction); + + } + + /** + * Process the node message loop until a break has been requested. + * + * @param env JNI env of the thread we're running in. + */ + void runNode() { + this->breakRequested = false; + while (1) { + uv_run(uv_default_loop(), UV_RUN_ONCE); + if (this->breakRequested) + break; + } + } + + /** + * Inject code into the running node instance. + * + * Must not be called from a different thread. + */ + void makeCallback(const char *code) { + mylog("in makeCallback"); + v8::Isolate::Scope isolate_scope(isolate); + v8::HandleScope handle_scope(isolate); + v8::Local context = globalContext.Get(isolate); + v8::Context::Scope context_scope(context); + v8::Local global = context->Global(); + v8::Local argv[] = { + v8::String::NewFromUtf8(isolate, code, + v8::NewStringType::kNormal).ToLocalChecked() + }; + mylog("calling node::MakeCallback"); + node::MakeCallback(isolate, global, "__akono_run", 1, argv, {0, 0}); + } + + ~NativeAkonoInstance() { + //this->isolate->Dispose(); + } + + jstring evalJs(JNIEnv *env, jstring sourceString) { + mylog("begin evalJs"); + + JStringValue jsv(env, sourceString); + + v8::Isolate::Scope isolate_scope(isolate); + + // Create a stack-allocated handle scope. + v8::HandleScope handle_scope(isolate); + + // Create a new context. + //v8::Local context = v8::Context::New(isolate); + + v8::Local context = globalContext.Get(isolate); + + // Enter the context for compiling and running the hello world script. + v8::Context::Scope context_scope(context); + + { + // Create a string containing the JavaScript source code. + v8::Local source = + v8::String::NewFromUtf8(isolate, *jsv, + v8::NewStringType::kNormal) + .ToLocalChecked(); + + // Compile the source code. + v8::Local script; + + mylog("about to compile script"); + + if (!v8::Script::Compile(context, source).ToLocal(&script)) { + return nullptr; + } + + mylog("about to run script"); + + // Run the script to get the result. + v8::Local result; + if (!script->Run(context).ToLocal(&result)) { + mylog("running script failed"); + return nullptr; + } + + mylog("converting script result value"); + + // Convert the result to an UTF8 string and print it. + v8::String::Utf8Value utf8(isolate, result); + + mylog("about to return value"); + + return env->NewStringUTF(*utf8); + } + } +}; + + +bool NativeAkonoInstance::v8Initialized = false; +bool NativeAkonoInstance::logInitialized = false; +node::MultiIsolatePlatform *NativeAkonoInstance::platform = nullptr; + + +void notifyCb(uv_async_t *async) { + NativeAkonoInstance *akono = (NativeAkonoInstance *) async->data; + mylog("async notifyCb called!"); + akono->breakRequested = true; +} + +static void sendMessageCallback(const v8::FunctionCallbackInfo &args) { + if (args.Length() < 1) return; + v8::Isolate *isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + v8::Local arg = args[0]; + v8::String::Utf8Value value(isolate, arg); + mylog("sendMessageCallback called, yay!"); + + v8::Local data = v8::Local::Cast(args.Data()); + + mylog("getting instance"); + NativeAkonoInstance *myInstance = (NativeAkonoInstance *) data->GetAlignedPointerFromInternalField(0); + + JNIEnv *env = myInstance->currentJniEnv; + + if (env == nullptr) { + mylog("FATAL: JNI env is nullptr"); + return; + } + + mylog("finding class"); + jclass clazz = env->FindClass("akono/AkonoJni"); + + if (clazz == nullptr) { + mylog("FATAL: class not found"); + return; + } + + mylog("creating strings"); + jstring jstr1 = env->NewStringUTF("message"); + jstring jstr2 = env->NewStringUTF(*value); + + mylog("getting method"); + + jmethodID meth = env->GetMethodID(clazz, "internalOnNotify", "(Ljava/lang/String;Ljava/lang/String;)V"); + + if (meth == nullptr) { + mylog("FATAL: method not found"); + return; + } + + mylog("calling method"); + + env->CallVoidMethod(myInstance->currentJniThiz, meth, jstr1, jstr2); +} + + +static void loadModuleCallback(const v8::FunctionCallbackInfo &args) { + if (args.Length() < 1) return; + v8::Isolate *isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + v8::Local arg = args[0]; + v8::String::Utf8Value value(isolate, arg); + mylog("sendMessageCallback called, yay!"); + + v8::Local data = v8::Local::Cast(args.Data()); + + mylog("getting instance"); + NativeAkonoInstance *myInstance = (NativeAkonoInstance *) data->GetAlignedPointerFromInternalField(0); + + JNIEnv *env = myInstance->currentJniEnv; + + if (env == nullptr) { + mylog("FATAL: JNI env is nullptr"); + return; + } + + mylog("finding class"); + jclass clazz = env->FindClass("akono/AkonoJni"); + + if (clazz == nullptr) { + mylog("FATAL: class not found"); + return; + } + + mylog("creating strings"); + jstring jstr1 = env->NewStringUTF(*value); + + mylog("getting method"); + + jmethodID meth = env->GetMethodID(clazz, "internalOnModuleLoad", "(Ljava/lang/String;)Ljava/lang/String;"); + + if (meth == nullptr) { + mylog("FATAL: method not found"); + return; + } + + mylog("calling method"); + + jstring jresult = (jstring) env->CallObjectMethod(myInstance->currentJniThiz, meth, jstr1); + + JStringValue resultStringValue(env, jresult); + + printf("before creating string, res %s\n", *resultStringValue); + + // Create a string containing the JavaScript source code. + v8::Local rs = + v8::String::NewFromUtf8(isolate, *resultStringValue, + v8::NewStringType::kNormal) + .ToLocalChecked(); + + args.GetReturnValue().Set(rs); +} + + +static void getDataCallback(const v8::FunctionCallbackInfo &args) { + if (args.Length() < 1) return; + v8::Isolate *isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + v8::Local arg = args[0]; + v8::String::Utf8Value value(isolate, arg); + mylog("getDataCallback called"); + + v8::Local data = v8::Local::Cast(args.Data()); + mylog("getting instance"); + NativeAkonoInstance *myInstance = (NativeAkonoInstance *) data->GetAlignedPointerFromInternalField(0); + + JNIEnv *env = myInstance->currentJniEnv; + if (env == nullptr) { + mylog("FATAL: JNI env is nullptr"); + return; + } + + mylog("finding class"); + jclass clazz = env->FindClass("akono/AkonoJni"); + + if (clazz == nullptr) { + mylog("FATAL: class not found"); + return; + } + + mylog("creating strings"); + jstring jstr1 = env->NewStringUTF(*value); + + mylog("getting method"); + + jmethodID meth = env->GetMethodID(clazz, "internalOnGetData", "(Ljava/lang/String;)Ljava/lang/String;"); + + if (meth == nullptr) { + mylog("FATAL: method not found"); + return; + } + + mylog("calling method"); + + jstring jresult = (jstring) env->CallObjectMethod(myInstance->currentJniThiz, meth, jstr1); + + JStringValue resultStringValue(env, jresult); + + printf("before creating string, res %s\n", *resultStringValue); + + // Create a string containing the JavaScript source code. + v8::Local rs = + v8::String::NewFromUtf8(isolate, *resultStringValue, + v8::NewStringType::kNormal) + .ToLocalChecked(); + + args.GetReturnValue().Set(rs); +} + + +extern "C" JNIEXPORT jobject JNICALL +Java_akono_AkonoJni_initNative(JNIEnv *env, jobject thiz) { + NativeAkonoInstance *myInstance = new NativeAkonoInstance(); + return env->NewDirectByteBuffer(myInstance, 0); +} + + +extern "C" JNIEXPORT void JNICALL +Java_akono_AkonoJni_destroyNative(JNIEnv *env, jobject thiz, jobject buf) { + NativeAkonoInstance *myInstance = (NativeAkonoInstance *) env->GetDirectBufferAddress(buf); + delete myInstance; +} + + +extern "C" JNIEXPORT jstring JNICALL +Java_akono_AkonoJni_evalJs(JNIEnv *env, jobject thiz, jstring sourceStr, jobject buf) { + NativeAkonoInstance *myInstance = (NativeAkonoInstance *) env->GetDirectBufferAddress(buf); + return myInstance->evalJs(env, sourceStr); +} + +extern "C" JNIEXPORT void JNICALL +Java_akono_AkonoJni_notifyNative(JNIEnv *env, jobject thiz, jobject buf) { + NativeAkonoInstance *myInstance = (NativeAkonoInstance *) env->GetDirectBufferAddress(buf); + uv_async_send(&myInstance->async_notify); +} + +extern "C" JNIEXPORT void JNICALL +Java_akono_AkonoJni_runNode(JNIEnv *env, jobject thiz, jobject buf) { + NativeAkonoInstance *myInstance = (NativeAkonoInstance *) env->GetDirectBufferAddress(buf); + myInstance->currentJniEnv = env; + myInstance->currentJniThiz = thiz; + myInstance->runNode(); +} + +extern "C" JNIEXPORT void JNICALL +Java_akono_AkonoJni_makeCallbackNative(JNIEnv *env, jobject thiz, jstring sourceStr, jobject buf) { + JStringValue jsv(env, sourceStr); + NativeAkonoInstance *myInstance = (NativeAkonoInstance *) env->GetDirectBufferAddress(buf); + myInstance->currentJniEnv = env; + myInstance->currentJniThiz = thiz; + return myInstance->makeCallback(*jsv); +} diff --git a/akono/src/main/java/akono/AkonoJni.kt b/akono/src/main/java/akono/AkonoJni.kt new file mode 100644 index 00000000..4a89a3f6 --- /dev/null +++ b/akono/src/main/java/akono/AkonoJni.kt @@ -0,0 +1,208 @@ +package akono + +import android.util.Base64 +import android.util.Log +import org.json.JSONObject +import java.lang.Exception +import java.nio.ByteBuffer +import java.util.concurrent.CountDownLatch +import java.util.concurrent.LinkedBlockingDeque +import kotlin.concurrent.thread + +typealias AkonoNativePointer = ByteBuffer + +data class ModuleResult(val path: String, val contents: String) + +class AkonoJni(vararg nodeArgv: String) { + private var getDataHandler: GetDataHandler? = null + private var messageHandler: MessageHandler? = null + private var loadModuleHandler: LoadModuleHandler? = null + private val initializedLatch = CountDownLatch(1) + + private val workQueue = LinkedBlockingDeque<() -> Unit>() + + private external fun evalJs(source: String, p: AkonoNativePointer): String + private external fun runNode(p: AkonoNativePointer) + + private external fun makeCallbackNative(source: String, p: AkonoNativePointer) + + private external fun destroyNative(b: AkonoNativePointer) + private external fun initNative(nodeArgv: Array): AkonoNativePointer + private external fun notifyNative(b: AkonoNativePointer) + + private lateinit var internalNativePointer: AkonoNativePointer + + private val jniThread: Thread + + private var stopped = false + + /** + * Schedule a block do be executed in the node thread. + */ + private fun scheduleNodeThread(b: () -> Unit) { + initializedLatch.await() + workQueue.put(b) + notifyNative() + } + + /** + * Called by node/v8 from its thread. + */ + @Suppress("unused") + private fun internalOnNotify(type: String, payload: String) { + Log.i("myapp", "internalOnNotify called") + Log.i("myapp", "type: $type") + Log.i("myapp", "payload: $payload") + messageHandler?.handleMessage(payload) + } + + /** + * Called by node/v8 from its thread. + */ + @Suppress("unused") + private fun internalOnModuleLoad(loadInfoStr: String): String { + Log.i("myapp", "internalOnModuleLoad called") + Log.i("myapp", "loadInfoStr is $loadInfoStr") + try { + val loadInfo = JSONObject(loadInfoStr) + val request: String = loadInfo.getString("request") + Log.i("myapp", "request is $request") + val handler = loadModuleHandler + if (handler != null) { + val modResult = handler.loadModule(request, arrayOf()) ?: return "null" + val result = JSONObject() + result.put("path", modResult.path) + result.put("content", modResult.contents) + return result.toString() + } else { + Log.v("myapp", "no module load handler registered") + return "null" + } + } catch (e: Exception) { + Log.v("myapp", "exception during internalOnModuleLoad: $e") + return "null" + } + } + + /** + * Called by node/v8 from its thread. + */ + @Suppress("unused") + private fun internalOnGetData(what: String): String? { + Log.i("myapp", "internalOnGetData called for $what") + val data = getDataHandler?.handleGetData(what) ?: return null + return Base64.encodeToString(data, Base64.NO_WRAP) + } + + + fun notifyNative() { + initializedLatch.await() + notifyNative(internalNativePointer) + } + + /** + * Schedule Node.JS to be run. + */ + fun evalSimpleJs(source: String): String { + val latch = CountDownLatch(1) + var result: String? = null + scheduleNodeThread { + result = evalJs(source, internalNativePointer) + latch.countDown() + } + latch.await() + return result ?: throw Exception("invariant failed") + } + + fun evalNodeCode(source: String) { + scheduleNodeThread { + makeCallbackNative(source, internalNativePointer) + } + } + + /** + * Send a message to node, calling global.__akono_onMessage. + */ + fun sendMessage(message: String) { + val encoded = Base64.encodeToString(message.toByteArray(), Base64.NO_WRAP) + val source = """ + if (global.__akono_onMessage) { + const msg = (new Buffer('$encoded', 'base64')).toString('ascii'); + global.__akono_onMessage(msg); + } else { + console.log("WARN: no __akono_onMessage defined"); + } + """.trimIndent() + evalNodeCode(source) + } + + /** + * + */ + fun waitStopped(): Unit { + Log.i("myapp", "waiting for stop") + scheduleNodeThread { + stopped = true + } + jniThread.join() + return + } + + /** + * Register a message handler that is called when the JavaScript code + * running in [runNodeJs] calls __akono_sendMessage + * + * Does not block. + */ + fun setMessageHandler(handler: MessageHandler) { + this.messageHandler = handler + } + + fun setLoadModuleHandler(handler: LoadModuleHandler) { + this.loadModuleHandler = handler + } + + fun setGetDataHandler(handler: GetDataHandler) { + this.getDataHandler = handler + } + + @Override + protected fun finalize() { + destroyNative(internalNativePointer) + } + + init { + jniThread = thread { + internalNativePointer = initNative(nodeArgv) + initializedLatch.countDown() + while (true) { + runNode(internalNativePointer) + while (true) { + val w = workQueue.poll() ?: break + w() + } + if (stopped) { + break + } + } + } + } + + companion object { + init { + System.loadLibrary("akono-jni") + } + } + + interface MessageHandler { + fun handleMessage(message: String) + } + + interface LoadModuleHandler { + fun loadModule(name: String, paths: Array): ModuleResult? + } + + interface GetDataHandler { + fun handleGetData(what: String): ByteArray? + } +} \ No newline at end of file diff --git a/akono/src/main/java/akono/Library.kt b/akono/src/main/java/akono/Library.kt new file mode 100644 index 00000000..920648fd --- /dev/null +++ b/akono/src/main/java/akono/Library.kt @@ -0,0 +1,10 @@ +/* + * This Kotlin source file was generated by the Gradle 'init' task. + */ +package akono + +class Library { + fun someLibraryMethod(): Boolean { + return true + } +} diff --git a/akono/src/test/java/akono/LibraryTest.kt b/akono/src/test/java/akono/LibraryTest.kt new file mode 100644 index 00000000..1a16e7e6 --- /dev/null +++ b/akono/src/test/java/akono/LibraryTest.kt @@ -0,0 +1,16 @@ +/* + * This Kotlin source file was generated by the Gradle 'init' task. + */ +package akono + +import kotlin.test.Test +import kotlin.test.assertTrue +import kotlin.test.assertEquals + +import akono.AkonoJni + +class LibraryTest { + @Test fun testSomeLibraryMethod() { + assertTrue(true) + } +} diff --git a/library/build.gradle.kts b/library/build.gradle.kts deleted file mode 100644 index b98510db..00000000 --- a/library/build.gradle.kts +++ /dev/null @@ -1,79 +0,0 @@ - -plugins { - id("com.android.library") - kotlin("android") - kotlin("android.extensions") -} - -android { - compileSdkVersion(28) - defaultConfig { - minSdkVersion(26) - targetSdkVersion(28) - - versionCode = 1 - versionName = "1.0" - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - // Specifies the application ID for the test APK. - testApplicationId = "akono.test" - - ndk { - // Tells Gradle to build outputs for the following ABIs and package - // them into your APK. - abiFilters("armeabi-v7a"); - } - - externalNativeBuild { - cmake.arguments("-DANDROID_STL=c++_shared") - } - } - useLibrary("android.test.runner") - useLibrary("android.test.base") - useLibrary("android.test.mock") - - externalNativeBuild { - cmake { - setPath(file("src/main/cpp/CMakeLists.txt")) - } - } - - sourceSets { - named("main") { - jniLibs.srcDirs("../deps/compiled") - } - } -} - -val kotlin_version: String by rootProject.extra - -repositories { - jcenter() -} - -dependencies { - //implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.3.20") - //implementation(kotlin("stdlib")) - - // Use the Kotlin test library. - testImplementation("org.jetbrains.kotlin:kotlin-test:$kotlin_version") - - // Use the Kotlin JUnit integration. - testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version") - - androidTestImplementation("androidx.test:core:1.1.0") - androidTestImplementation("androidx.test:runner:1.1.1") - androidTestImplementation("androidx.test:rules:1.1.1") - - // Assertions - androidTestImplementation("androidx.test.ext:junit:1.1.0") - androidTestImplementation("androidx.test.ext:truth:1.1.0") - androidTestImplementation("com.google.truth:truth:0.44") - - // Use the Kotlin test library. - androidTestImplementation("org.jetbrains.kotlin:kotlin-test:$kotlin_version") - - // Use the Kotlin JUnit integration. - androidTestImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version") - implementation(kotlin("stdlib-jdk7", kotlin_version)) -} diff --git a/library/src/androidTest/java/akono/InstrumentedAkonoTests.kt b/library/src/androidTest/java/akono/InstrumentedAkonoTests.kt deleted file mode 100644 index beda5119..00000000 --- a/library/src/androidTest/java/akono/InstrumentedAkonoTests.kt +++ /dev/null @@ -1,85 +0,0 @@ -package akono.test; - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import org.junit.runner.RunWith -import org.junit.Test -import androidx.test.filters.LargeTest -import org.junit.Assert.assertEquals -import akono.AkonoJni -import akono.ModuleResult -import android.util.Log -import java.util.concurrent.LinkedBlockingDeque - - -class SyncMessageHandler : AkonoJni.MessageHandler { - private val messageQueue = LinkedBlockingDeque() - override fun handleMessage(message: String) { - messageQueue.put(message) - } - - fun waitForMessage(): String { - return messageQueue.take() - } -} - - -class StaticModuleLoadHandler : AkonoJni.LoadModuleHandler { - private val modules: MutableMap = HashMap() - - override fun loadModule(name: String, paths: Array): ModuleResult? { - val code = modules.get(name) ?: return null - if (modules.containsKey(name)) { - return ModuleResult("/vmodroot/$name.js", code) - } - return null - } - - fun registerModule(name: String, source: String) { - modules[name] = source - } -} - - -// @RunWith is required only if you use a mix of JUnit3 and JUnit4. -@RunWith(AndroidJUnit4::class) -@LargeTest -public class InstrumentedAkonoTestOne { - @Test - fun myJsTest() { - val ajni: AkonoJni = AkonoJni() - assertEquals("2", ajni.evalSimpleJs("1+1")) - assertEquals("36", ajni.evalSimpleJs("6*6")) - assertEquals("42", ajni.evalSimpleJs("(()=>{let x = 42; return x;})()")) - assertEquals("undefined", ajni.evalSimpleJs("const myVal = 42")) - assertEquals("43", ajni.evalSimpleJs("myVal + 1")) - - val myHandler = SyncMessageHandler() - ajni.setMessageHandler(myHandler) - ajni.evalNodeCode("console.log('hi from the test case')") - // Tell the message handler to just ping back messages to us - ajni.evalNodeCode("global.__akono_onMessage = (x) => { global.__akono_sendMessage(x); }") - val sentMessage = "Hello AKONO!!" - ajni.sendMessage(sentMessage) - val receivedMessage = myHandler.waitForMessage() - assertEquals(sentMessage, receivedMessage) - Log.i("myapp", "test case received message: $receivedMessage") - - val myModHandler = StaticModuleLoadHandler() - - ajni.setLoadModuleHandler(myModHandler) - - myModHandler.registerModule("a", """ - |console.log('I am module a'); - |exports.foo = () => { global.__akono_sendMessage('hello42'); }; - """.trimMargin()) - - ajni.evalNodeCode("a = require('a');") - ajni.evalNodeCode("a.foo()") - - val msg2 = myHandler.waitForMessage() - - assertEquals("hello42", msg2) - - ajni.waitStopped() - } -} diff --git a/library/src/main/AndroidManifest.xml b/library/src/main/AndroidManifest.xml deleted file mode 100644 index 3a8d4915..00000000 --- a/library/src/main/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/library/src/main/cpp/CMakeLists.txt b/library/src/main/cpp/CMakeLists.txt deleted file mode 100644 index 10d6396f..00000000 --- a/library/src/main/cpp/CMakeLists.txt +++ /dev/null @@ -1,42 +0,0 @@ -cmake_minimum_required(VERSION 3.4.1) - -add_library( - akono-jni SHARED - akono-jni.cpp -) - -set(deps_dir ${CMAKE_CURRENT_SOURCE_DIR}/../../../../deps) - -if(NOT EXISTS ${deps_dir}) - message( FATAL_ERROR "Dependency directory does not exist") -endif() - -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++14") - -include_directories( - ${deps_dir}/android-node-v8/src - ${deps_dir}/android-node-v8/deps/v8/include - ${deps_dir}/android-node-v8/deps/uv/include -) - -add_library(node SHARED IMPORTED) -set_target_properties(node PROPERTIES IMPORTED_LOCATION - ${deps_dir}/compiled/${ANDROID_ABI}/libnode.so) - -add_library(v8 STATIC IMPORTED) -set_target_properties(v8 PROPERTIES IMPORTED_LOCATION - ${deps_dir}/compiled/${ANDROID_ABI}/libv8.cr.so) - -add_library(v8_platform STATIC IMPORTED) -set_target_properties(v8_platform PROPERTIES IMPORTED_LOCATION - ${deps_dir}/compiled/${ANDROID_ABI}/libv8_libplatform.cr.so) - -# Include libraries needed for hello-jni lib -target_link_libraries(akono-jni - v8 - v8_platform - node - android - log) - - diff --git a/library/src/main/cpp/akono-jni.cpp b/library/src/main/cpp/akono-jni.cpp deleted file mode 100644 index b9e71012..00000000 --- a/library/src/main/cpp/akono-jni.cpp +++ /dev/null @@ -1,588 +0,0 @@ -#include -#include -#include -#include - -#define NODE_WANT_INTERNALS 1 - -#include - -#include -#include -#include -#include -#include - - - -// Provide stubs so that libnode.so links properly -namespace node { - namespace native_module { - const bool has_code_cache = false; - - void NativeModuleEnv::InitializeCodeCache() {} - } - - v8::StartupData *NodeMainInstance::GetEmbeddedSnapshotBlob() { - return nullptr; - } - - const std::vector *NodeMainInstance::GetIsolateDataIndexes() { - return nullptr; - } -} - - -static int pfd[2]; -static pthread_t thr; -static const char *tag = "myapp"; - - -static void *thread_func(void *) { - ssize_t rdsz; - char buf[128]; - while ((rdsz = read(pfd[0], buf, sizeof buf - 1)) > 0) { - if (buf[rdsz - 1] == '\n') --rdsz; - buf[rdsz] = 0; /* add null-terminator */ - __android_log_write(ANDROID_LOG_DEBUG, tag, buf); - } - return 0; -} - -static void mylog(const char *msg) { - __android_log_write(ANDROID_LOG_DEBUG, tag, msg); -} - -int start_logger(const char *app_name) { - tag = app_name; - - /* make stdout line-buffered and stderr unbuffered */ - setvbuf(stdout, 0, _IOLBF, 0); - setvbuf(stderr, 0, _IONBF, 0); - - /* create the pipe and redirect stdout and stderr */ - pipe(pfd); - dup2(pfd[1], 1); - dup2(pfd[1], 2); - - /* spawn the logging thread */ - if (pthread_create(&thr, 0, thread_func, 0) == -1) - return -1; - pthread_detach(thr); - return 0; -} - - -/** - * Helper class to manage conversion from a JNI string to a C string. - */ -class JStringValue { -private: - jstring m_jstr; - const char *m_cstr; - JNIEnv *m_env; -public: - JStringValue(JNIEnv *env, jstring s) : m_env(env), m_jstr(s) { - m_cstr = env->GetStringUTFChars(s, NULL); - } - - ~JStringValue() { - m_env->ReleaseStringUTFChars(m_jstr, m_cstr); - } - - const char *operator*() { - return m_cstr; - } -}; - - -/** - * Slightly more sane wrapper around node::Init - */ -static void InitNode(std::vector argv) { - int ret_exec_argc = 0; - int ret_argc = argv.size(); - const char **ret_exec_argv = nullptr; - - node::Init(&ret_argc, &argv[0], &ret_exec_argc, &ret_exec_argv); -} - -// Forward declarations -void notifyCb(uv_async_t *async); - -static void sendMessageCallback(const v8::FunctionCallbackInfo &args); -static void loadModuleCallback(const v8::FunctionCallbackInfo &args); -static void getDataCallback(const v8::FunctionCallbackInfo &args); - -static const char *main_code = "global.__akono_run = (x) => {" - " console.log('running code', x);" - " global.eval(x);" - "};" - "" - "global.__akono_onMessage = (x) => {" - " console.log('got __akono_onMessage', x);" - "};" - "" - "mod = require('module');" - "mod._saved_findPath = mod._findPath;" - "mod._akonoMods = {};" - "mod._findPath = (request, paths, isMain) => {" - " console.log('in _findPath');" - " const res = mod._saved_findPath(request, paths, isMain);" - " if (res !== false) return res;" - " const args = JSON.stringify({ request, paths});" - " const loadResult = JSON.parse(global.__akono_loadModule(args));" - " console.log('got loadModule result', loadResult);" - " if (!loadResult) return false;" - " mod._akonoMods[loadResult.path] = loadResult;" - " console.log('returning path', loadResult.path);" - " return loadResult.path;" - "};" - "" - "function stripBOM(content) {" - " if (content.charCodeAt(0) === 0xFEFF) {" - " content = content.slice(1);" - " }" - " return content;" - "}" - "" - "mod._saved_js_extension = mod._extensions[\".js\"];" - "mod._extensions[\".js\"] = (module, filename) => {" - " console.log('handling js extension', [module, filename]);" - " if (mod._akonoMods.hasOwnProperty(filename)) {" - " const akmod = mod._akonoMods[filename];" - " console.log('found mod', akmod);" - " const content = akmod.content;" - " return module._compile(stripBOM(content), filename);" - " }" - " console.log('falling back');" - " return mod._saved_js_extension(module, filename);" - "};"; - - -class NativeAkonoInstance { -private: - static bool logInitialized; - static bool v8Initialized; - //static std::unique_ptr platform; - static node::MultiIsolatePlatform *platform; -public: - v8::Isolate *isolate; - node::Environment *environment; - v8::Persistent globalContext; - uv_async_t async_notify; - uv_loop_t *loop; - bool breakRequested = false; - JNIEnv *currentJniEnv = nullptr; - jobject currentJniThiz = nullptr; - - NativeAkonoInstance() : globalContext() { - loop = uv_default_loop(); - uv_async_init(loop, &async_notify, notifyCb); - async_notify.data = this; - - if (!logInitialized) { - start_logger("myapp"); - logInitialized = true; - } - - if (!v8Initialized) { - //platform = v8::platform::NewDefaultPlatform(); - //v8::V8::InitializePlatform(platform.get()); - //v8::V8::Initialize(); - - // Here, only the arguments used to initialize the global node/v8 platform - // are relevant, the others are skipped. - InitNode(std::vector{"node", "-e", main_code}); - platform = node::InitializeV8Platform(10); - v8::V8::Initialize(); - - v8Initialized = true; - } - - node::ArrayBufferAllocator *allocator = node::CreateArrayBufferAllocator(); - this->isolate = node::NewIsolate(allocator, uv_default_loop()); - - - v8::Isolate::Scope isolate_scope(isolate); - v8::HandleScope handle_scope(isolate); - - node::IsolateData *isolateData = node::CreateIsolateData( - this->isolate, - uv_default_loop(), - platform, - allocator); - - - globalContext.Reset(isolate, node::NewContext(isolate)); - - v8::Local context = globalContext.Get(isolate); - - // Arguments for node itself - std::vector nodeArgv{"node", "-e", "console.log('hello world');"}; - // Arguments for the script run by node - std::vector nodeExecArgv{}; - - mylog("entering global scopt"); - - v8::Context::Scope context_scope(globalContext.Get(isolate)); - - mylog("creating environment"); - - node::Environment *environment = node::CreateEnvironment( - isolateData, - globalContext.Get(isolate), - nodeArgv.size(), - &nodeArgv[0], - nodeExecArgv.size(), - &nodeExecArgv[0]); - - - mylog("loading environment"); - - node::LoadEnvironment(environment); - - mylog("finished loading environment"); - - v8::Local dataTemplate = v8::ObjectTemplate::New(isolate); - dataTemplate->SetInternalFieldCount(1); - v8::Local dataObject = dataTemplate->NewInstance(context).ToLocalChecked(); - dataObject->SetAlignedPointerInInternalField(0, this); - - v8::Local sendMessageFunction = v8::Function::New(context, - sendMessageCallback, - dataObject).ToLocalChecked(); - - v8::Local loadModuleFunction = v8::Function::New(context, - loadModuleCallback, - dataObject).ToLocalChecked(); - - v8::Local getDataFunction = v8::Function::New(context, - getDataCallback, - dataObject).ToLocalChecked(); - - v8::Local global = context->Global(); - - global->Set(v8::String::NewFromUtf8(isolate, "__akono_sendMessage", - v8::NewStringType::kNormal).ToLocalChecked(), - sendMessageFunction); - - global->Set(v8::String::NewFromUtf8(isolate, "__akono_loadModule", - v8::NewStringType::kNormal).ToLocalChecked(), - loadModuleFunction); - - // Get data synchronously (!) from the embedder - global->Set(v8::String::NewFromUtf8(isolate, "__akono_getData", - v8::NewStringType::kNormal).ToLocalChecked(), - getDataFunction); - - } - - /** - * Process the node message loop until a break has been requested. - * - * @param env JNI env of the thread we're running in. - */ - void runNode() { - this->breakRequested = false; - while (1) { - uv_run(uv_default_loop(), UV_RUN_ONCE); - if (this->breakRequested) - break; - } - } - - /** - * Inject code into the running node instance. - * - * Must not be called from a different thread. - */ - void makeCallback(const char *code) { - mylog("in makeCallback"); - v8::Isolate::Scope isolate_scope(isolate); - v8::HandleScope handle_scope(isolate); - v8::Local context = globalContext.Get(isolate); - v8::Context::Scope context_scope(context); - v8::Local global = context->Global(); - v8::Local argv[] = { - v8::String::NewFromUtf8(isolate, code, - v8::NewStringType::kNormal).ToLocalChecked() - }; - mylog("calling node::MakeCallback"); - node::MakeCallback(isolate, global, "__akono_run", 1, argv, {0, 0}); - } - - ~NativeAkonoInstance() { - //this->isolate->Dispose(); - } - - jstring evalJs(JNIEnv *env, jstring sourceString) { - mylog("begin evalJs"); - - JStringValue jsv(env, sourceString); - - v8::Isolate::Scope isolate_scope(isolate); - - // Create a stack-allocated handle scope. - v8::HandleScope handle_scope(isolate); - - // Create a new context. - //v8::Local context = v8::Context::New(isolate); - - v8::Local context = globalContext.Get(isolate); - - // Enter the context for compiling and running the hello world script. - v8::Context::Scope context_scope(context); - - { - // Create a string containing the JavaScript source code. - v8::Local source = - v8::String::NewFromUtf8(isolate, *jsv, - v8::NewStringType::kNormal) - .ToLocalChecked(); - - // Compile the source code. - v8::Local script; - - mylog("about to compile script"); - - if (!v8::Script::Compile(context, source).ToLocal(&script)) { - return nullptr; - } - - mylog("about to run script"); - - // Run the script to get the result. - v8::Local result; - if (!script->Run(context).ToLocal(&result)) { - mylog("running script failed"); - return nullptr; - } - - mylog("converting script result value"); - - // Convert the result to an UTF8 string and print it. - v8::String::Utf8Value utf8(isolate, result); - - mylog("about to return value"); - - return env->NewStringUTF(*utf8); - } - } -}; - - -bool NativeAkonoInstance::v8Initialized = false; -bool NativeAkonoInstance::logInitialized = false; -node::MultiIsolatePlatform *NativeAkonoInstance::platform = nullptr; - - -void notifyCb(uv_async_t *async) { - NativeAkonoInstance *akono = (NativeAkonoInstance *) async->data; - mylog("async notifyCb called!"); - akono->breakRequested = true; -} - -static void sendMessageCallback(const v8::FunctionCallbackInfo &args) { - if (args.Length() < 1) return; - v8::Isolate *isolate = args.GetIsolate(); - v8::HandleScope scope(isolate); - v8::Local arg = args[0]; - v8::String::Utf8Value value(isolate, arg); - mylog("sendMessageCallback called, yay!"); - - v8::Local data = v8::Local::Cast(args.Data()); - - mylog("getting instance"); - NativeAkonoInstance *myInstance = (NativeAkonoInstance *) data->GetAlignedPointerFromInternalField(0); - - JNIEnv *env = myInstance->currentJniEnv; - - if (env == nullptr) { - mylog("FATAL: JNI env is nullptr"); - return; - } - - mylog("finding class"); - jclass clazz = env->FindClass("akono/AkonoJni"); - - if (clazz == nullptr) { - mylog("FATAL: class not found"); - return; - } - - mylog("creating strings"); - jstring jstr1 = env->NewStringUTF("message"); - jstring jstr2 = env->NewStringUTF(*value); - - mylog("getting method"); - - jmethodID meth = env->GetMethodID(clazz, "internalOnNotify", "(Ljava/lang/String;Ljava/lang/String;)V"); - - if (meth == nullptr) { - mylog("FATAL: method not found"); - return; - } - - mylog("calling method"); - - env->CallVoidMethod(myInstance->currentJniThiz, meth, jstr1, jstr2); -} - - -static void loadModuleCallback(const v8::FunctionCallbackInfo &args) { - if (args.Length() < 1) return; - v8::Isolate *isolate = args.GetIsolate(); - v8::HandleScope scope(isolate); - v8::Local arg = args[0]; - v8::String::Utf8Value value(isolate, arg); - mylog("sendMessageCallback called, yay!"); - - v8::Local data = v8::Local::Cast(args.Data()); - - mylog("getting instance"); - NativeAkonoInstance *myInstance = (NativeAkonoInstance *) data->GetAlignedPointerFromInternalField(0); - - JNIEnv *env = myInstance->currentJniEnv; - - if (env == nullptr) { - mylog("FATAL: JNI env is nullptr"); - return; - } - - mylog("finding class"); - jclass clazz = env->FindClass("akono/AkonoJni"); - - if (clazz == nullptr) { - mylog("FATAL: class not found"); - return; - } - - mylog("creating strings"); - jstring jstr1 = env->NewStringUTF(*value); - - mylog("getting method"); - - jmethodID meth = env->GetMethodID(clazz, "internalOnModuleLoad", "(Ljava/lang/String;)Ljava/lang/String;"); - - if (meth == nullptr) { - mylog("FATAL: method not found"); - return; - } - - mylog("calling method"); - - jstring jresult = (jstring) env->CallObjectMethod(myInstance->currentJniThiz, meth, jstr1); - - JStringValue resultStringValue(env, jresult); - - printf("before creating string, res %s\n", *resultStringValue); - - // Create a string containing the JavaScript source code. - v8::Local rs = - v8::String::NewFromUtf8(isolate, *resultStringValue, - v8::NewStringType::kNormal) - .ToLocalChecked(); - - args.GetReturnValue().Set(rs); -} - - -static void getDataCallback(const v8::FunctionCallbackInfo &args) { - if (args.Length() < 1) return; - v8::Isolate *isolate = args.GetIsolate(); - v8::HandleScope scope(isolate); - v8::Local arg = args[0]; - v8::String::Utf8Value value(isolate, arg); - mylog("getDataCallback called"); - - v8::Local data = v8::Local::Cast(args.Data()); - mylog("getting instance"); - NativeAkonoInstance *myInstance = (NativeAkonoInstance *) data->GetAlignedPointerFromInternalField(0); - - JNIEnv *env = myInstance->currentJniEnv; - if (env == nullptr) { - mylog("FATAL: JNI env is nullptr"); - return; - } - - mylog("finding class"); - jclass clazz = env->FindClass("akono/AkonoJni"); - - if (clazz == nullptr) { - mylog("FATAL: class not found"); - return; - } - - mylog("creating strings"); - jstring jstr1 = env->NewStringUTF(*value); - - mylog("getting method"); - - jmethodID meth = env->GetMethodID(clazz, "internalOnGetData", "(Ljava/lang/String;)Ljava/lang/String;"); - - if (meth == nullptr) { - mylog("FATAL: method not found"); - return; - } - - mylog("calling method"); - - jstring jresult = (jstring) env->CallObjectMethod(myInstance->currentJniThiz, meth, jstr1); - - JStringValue resultStringValue(env, jresult); - - printf("before creating string, res %s\n", *resultStringValue); - - // Create a string containing the JavaScript source code. - v8::Local rs = - v8::String::NewFromUtf8(isolate, *resultStringValue, - v8::NewStringType::kNormal) - .ToLocalChecked(); - - args.GetReturnValue().Set(rs); -} - - -extern "C" JNIEXPORT jobject JNICALL -Java_akono_AkonoJni_initNative(JNIEnv *env, jobject thiz) { - NativeAkonoInstance *myInstance = new NativeAkonoInstance(); - return env->NewDirectByteBuffer(myInstance, 0); -} - - -extern "C" JNIEXPORT void JNICALL -Java_akono_AkonoJni_destroyNative(JNIEnv *env, jobject thiz, jobject buf) { - NativeAkonoInstance *myInstance = (NativeAkonoInstance *) env->GetDirectBufferAddress(buf); - delete myInstance; -} - - -extern "C" JNIEXPORT jstring JNICALL -Java_akono_AkonoJni_evalJs(JNIEnv *env, jobject thiz, jstring sourceStr, jobject buf) { - NativeAkonoInstance *myInstance = (NativeAkonoInstance *) env->GetDirectBufferAddress(buf); - return myInstance->evalJs(env, sourceStr); -} - -extern "C" JNIEXPORT void JNICALL -Java_akono_AkonoJni_notifyNative(JNIEnv *env, jobject thiz, jobject buf) { - NativeAkonoInstance *myInstance = (NativeAkonoInstance *) env->GetDirectBufferAddress(buf); - uv_async_send(&myInstance->async_notify); -} - -extern "C" JNIEXPORT void JNICALL -Java_akono_AkonoJni_runNode(JNIEnv *env, jobject thiz, jobject buf) { - NativeAkonoInstance *myInstance = (NativeAkonoInstance *) env->GetDirectBufferAddress(buf); - myInstance->currentJniEnv = env; - myInstance->currentJniThiz = thiz; - myInstance->runNode(); -} - -extern "C" JNIEXPORT void JNICALL -Java_akono_AkonoJni_makeCallbackNative(JNIEnv *env, jobject thiz, jstring sourceStr, jobject buf) { - JStringValue jsv(env, sourceStr); - NativeAkonoInstance *myInstance = (NativeAkonoInstance *) env->GetDirectBufferAddress(buf); - myInstance->currentJniEnv = env; - myInstance->currentJniThiz = thiz; - return myInstance->makeCallback(*jsv); -} diff --git a/library/src/main/java/akono/AkonoJni.kt b/library/src/main/java/akono/AkonoJni.kt deleted file mode 100644 index 4a89a3f6..00000000 --- a/library/src/main/java/akono/AkonoJni.kt +++ /dev/null @@ -1,208 +0,0 @@ -package akono - -import android.util.Base64 -import android.util.Log -import org.json.JSONObject -import java.lang.Exception -import java.nio.ByteBuffer -import java.util.concurrent.CountDownLatch -import java.util.concurrent.LinkedBlockingDeque -import kotlin.concurrent.thread - -typealias AkonoNativePointer = ByteBuffer - -data class ModuleResult(val path: String, val contents: String) - -class AkonoJni(vararg nodeArgv: String) { - private var getDataHandler: GetDataHandler? = null - private var messageHandler: MessageHandler? = null - private var loadModuleHandler: LoadModuleHandler? = null - private val initializedLatch = CountDownLatch(1) - - private val workQueue = LinkedBlockingDeque<() -> Unit>() - - private external fun evalJs(source: String, p: AkonoNativePointer): String - private external fun runNode(p: AkonoNativePointer) - - private external fun makeCallbackNative(source: String, p: AkonoNativePointer) - - private external fun destroyNative(b: AkonoNativePointer) - private external fun initNative(nodeArgv: Array): AkonoNativePointer - private external fun notifyNative(b: AkonoNativePointer) - - private lateinit var internalNativePointer: AkonoNativePointer - - private val jniThread: Thread - - private var stopped = false - - /** - * Schedule a block do be executed in the node thread. - */ - private fun scheduleNodeThread(b: () -> Unit) { - initializedLatch.await() - workQueue.put(b) - notifyNative() - } - - /** - * Called by node/v8 from its thread. - */ - @Suppress("unused") - private fun internalOnNotify(type: String, payload: String) { - Log.i("myapp", "internalOnNotify called") - Log.i("myapp", "type: $type") - Log.i("myapp", "payload: $payload") - messageHandler?.handleMessage(payload) - } - - /** - * Called by node/v8 from its thread. - */ - @Suppress("unused") - private fun internalOnModuleLoad(loadInfoStr: String): String { - Log.i("myapp", "internalOnModuleLoad called") - Log.i("myapp", "loadInfoStr is $loadInfoStr") - try { - val loadInfo = JSONObject(loadInfoStr) - val request: String = loadInfo.getString("request") - Log.i("myapp", "request is $request") - val handler = loadModuleHandler - if (handler != null) { - val modResult = handler.loadModule(request, arrayOf()) ?: return "null" - val result = JSONObject() - result.put("path", modResult.path) - result.put("content", modResult.contents) - return result.toString() - } else { - Log.v("myapp", "no module load handler registered") - return "null" - } - } catch (e: Exception) { - Log.v("myapp", "exception during internalOnModuleLoad: $e") - return "null" - } - } - - /** - * Called by node/v8 from its thread. - */ - @Suppress("unused") - private fun internalOnGetData(what: String): String? { - Log.i("myapp", "internalOnGetData called for $what") - val data = getDataHandler?.handleGetData(what) ?: return null - return Base64.encodeToString(data, Base64.NO_WRAP) - } - - - fun notifyNative() { - initializedLatch.await() - notifyNative(internalNativePointer) - } - - /** - * Schedule Node.JS to be run. - */ - fun evalSimpleJs(source: String): String { - val latch = CountDownLatch(1) - var result: String? = null - scheduleNodeThread { - result = evalJs(source, internalNativePointer) - latch.countDown() - } - latch.await() - return result ?: throw Exception("invariant failed") - } - - fun evalNodeCode(source: String) { - scheduleNodeThread { - makeCallbackNative(source, internalNativePointer) - } - } - - /** - * Send a message to node, calling global.__akono_onMessage. - */ - fun sendMessage(message: String) { - val encoded = Base64.encodeToString(message.toByteArray(), Base64.NO_WRAP) - val source = """ - if (global.__akono_onMessage) { - const msg = (new Buffer('$encoded', 'base64')).toString('ascii'); - global.__akono_onMessage(msg); - } else { - console.log("WARN: no __akono_onMessage defined"); - } - """.trimIndent() - evalNodeCode(source) - } - - /** - * - */ - fun waitStopped(): Unit { - Log.i("myapp", "waiting for stop") - scheduleNodeThread { - stopped = true - } - jniThread.join() - return - } - - /** - * Register a message handler that is called when the JavaScript code - * running in [runNodeJs] calls __akono_sendMessage - * - * Does not block. - */ - fun setMessageHandler(handler: MessageHandler) { - this.messageHandler = handler - } - - fun setLoadModuleHandler(handler: LoadModuleHandler) { - this.loadModuleHandler = handler - } - - fun setGetDataHandler(handler: GetDataHandler) { - this.getDataHandler = handler - } - - @Override - protected fun finalize() { - destroyNative(internalNativePointer) - } - - init { - jniThread = thread { - internalNativePointer = initNative(nodeArgv) - initializedLatch.countDown() - while (true) { - runNode(internalNativePointer) - while (true) { - val w = workQueue.poll() ?: break - w() - } - if (stopped) { - break - } - } - } - } - - companion object { - init { - System.loadLibrary("akono-jni") - } - } - - interface MessageHandler { - fun handleMessage(message: String) - } - - interface LoadModuleHandler { - fun loadModule(name: String, paths: Array): ModuleResult? - } - - interface GetDataHandler { - fun handleGetData(what: String): ByteArray? - } -} \ No newline at end of file diff --git a/library/src/main/java/akono/Library.kt b/library/src/main/java/akono/Library.kt deleted file mode 100644 index 920648fd..00000000 --- a/library/src/main/java/akono/Library.kt +++ /dev/null @@ -1,10 +0,0 @@ -/* - * This Kotlin source file was generated by the Gradle 'init' task. - */ -package akono - -class Library { - fun someLibraryMethod(): Boolean { - return true - } -} diff --git a/library/src/test/java/akono/LibraryTest.kt b/library/src/test/java/akono/LibraryTest.kt deleted file mode 100644 index 1a16e7e6..00000000 --- a/library/src/test/java/akono/LibraryTest.kt +++ /dev/null @@ -1,16 +0,0 @@ -/* - * This Kotlin source file was generated by the Gradle 'init' task. - */ -package akono - -import kotlin.test.Test -import kotlin.test.assertTrue -import kotlin.test.assertEquals - -import akono.AkonoJni - -class LibraryTest { - @Test fun testSomeLibraryMethod() { - assertTrue(true) - } -} diff --git a/settings.gradle.kts b/settings.gradle.kts index caca0e0c..0804e454 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -9,4 +9,4 @@ rootProject.name = "akono" -include(":library") +include(":akono") -- cgit v1.2.3