summaryrefslogtreecommitdiff
path: root/akono
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2019-08-14 16:54:11 +0200
committerFlorian Dold <florian.dold@gmail.com>2019-08-14 16:54:11 +0200
commitf6141fd410855a82b9bbaa02a0b2634ed137843c (patch)
tree396bca4167404f7673bdf76163fd83cba9dd4c0f /akono
parent66eb46b1a40c088b1096ba2f294796404b1a530c (diff)
downloadakono-f6141fd410855a82b9bbaa02a0b2634ed137843c.tar.gz
akono-f6141fd410855a82b9bbaa02a0b2634ed137843c.tar.bz2
akono-f6141fd410855a82b9bbaa02a0b2634ed137843c.zip
rename library->akono
Diffstat (limited to 'akono')
-rw-r--r--akono/build.gradle.kts79
-rw-r--r--akono/src/androidTest/java/akono/InstrumentedAkonoTests.kt85
-rw-r--r--akono/src/main/AndroidManifest.xml7
-rw-r--r--akono/src/main/cpp/CMakeLists.txt42
-rw-r--r--akono/src/main/cpp/akono-jni.cpp588
-rw-r--r--akono/src/main/java/akono/AkonoJni.kt208
-rw-r--r--akono/src/main/java/akono/Library.kt10
-rw-r--r--akono/src/test/java/akono/LibraryTest.kt16
8 files changed, 1035 insertions, 0 deletions
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<String>()
+ override fun handleMessage(message: String) {
+ messageQueue.put(message)
+ }
+
+ fun waitForMessage(): String {
+ return messageQueue.take()
+ }
+}
+
+
+class StaticModuleLoadHandler : AkonoJni.LoadModuleHandler {
+ private val modules: MutableMap<String, String> = HashMap()
+
+ override fun loadModule(name: String, paths: Array<String>): 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 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="net.taler.akono">
+
+ <uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+
+</manifest>
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 <string.h>
+#include <jni.h>
+#include <libplatform/libplatform.h>
+#include <v8.h>
+
+#define NODE_WANT_INTERNALS 1
+
+#include <node.h>
+
+#include <uv.h>
+#include <unistd.h>
+#include <android/log.h>
+#include <node_main_instance.h>
+#include <node_native_module_env.h>
+
+
+
+// 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<size_t> *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<const char *> 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<v8::Value> &args);
+static void loadModuleCallback(const v8::FunctionCallbackInfo<v8::Value> &args);
+static void getDataCallback(const v8::FunctionCallbackInfo<v8::Value> &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<v8::Platform> platform;
+ static node::MultiIsolatePlatform *platform;
+public:
+ v8::Isolate *isolate;
+ node::Environment *environment;
+ v8::Persistent<v8::Context> 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<const char *>{"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<v8::Context> context = globalContext.Get(isolate);
+
+ // Arguments for node itself
+ std::vector<const char *> nodeArgv{"node", "-e", "console.log('hello world');"};
+ // Arguments for the script run by node
+ std::vector<const char *> 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<v8::ObjectTemplate> dataTemplate = v8::ObjectTemplate::New(isolate);
+ dataTemplate->SetInternalFieldCount(1);
+ v8::Local<v8::Object> dataObject = dataTemplate->NewInstance(context).ToLocalChecked();
+ dataObject->SetAlignedPointerInInternalField(0, this);
+
+ v8::Local<v8::Function> sendMessageFunction = v8::Function::New(context,
+ sendMessageCallback,
+ dataObject).ToLocalChecked();
+
+ v8::Local<v8::Function> loadModuleFunction = v8::Function::New(context,
+ loadModuleCallback,
+ dataObject).ToLocalChecked();
+
+ v8::Local<v8::Function> getDataFunction = v8::Function::New(context,
+ getDataCallback,
+ dataObject).ToLocalChecked();
+
+ v8::Local<v8::Object> 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<v8::Context> context = globalContext.Get(isolate);
+ v8::Context::Scope context_scope(context);
+ v8::Local<v8::Object> global = context->Global();
+ v8::Local<v8::Value> 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<v8::Context> context = v8::Context::New(isolate);
+
+ v8::Local<v8::Context> 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<v8::String> source =
+ v8::String::NewFromUtf8(isolate, *jsv,
+ v8::NewStringType::kNormal)
+ .ToLocalChecked();
+
+ // Compile the source code.
+ v8::Local<v8::Script> 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<v8::Value> 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<v8::Value> &args) {
+ if (args.Length() < 1) return;
+ v8::Isolate *isolate = args.GetIsolate();
+ v8::HandleScope scope(isolate);
+ v8::Local<v8::Value> arg = args[0];
+ v8::String::Utf8Value value(isolate, arg);
+ mylog("sendMessageCallback called, yay!");
+
+ v8::Local<v8::Object> data = v8::Local<v8::Object>::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<v8::Value> &args) {
+ if (args.Length() < 1) return;
+ v8::Isolate *isolate = args.GetIsolate();
+ v8::HandleScope scope(isolate);
+ v8::Local<v8::Value> arg = args[0];
+ v8::String::Utf8Value value(isolate, arg);
+ mylog("sendMessageCallback called, yay!");
+
+ v8::Local<v8::Object> data = v8::Local<v8::Object>::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<v8::String> rs =
+ v8::String::NewFromUtf8(isolate, *resultStringValue,
+ v8::NewStringType::kNormal)
+ .ToLocalChecked();
+
+ args.GetReturnValue().Set(rs);
+}
+
+
+static void getDataCallback(const v8::FunctionCallbackInfo<v8::Value> &args) {
+ if (args.Length() < 1) return;
+ v8::Isolate *isolate = args.GetIsolate();
+ v8::HandleScope scope(isolate);
+ v8::Local<v8::Value> arg = args[0];
+ v8::String::Utf8Value value(isolate, arg);
+ mylog("getDataCallback called");
+
+ v8::Local<v8::Object> data = v8::Local<v8::Object>::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<v8::String> 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<out String>): 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<String>): 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)
+ }
+}