windows wrapper

This commit is contained in:
ekibun
2020-09-21 01:41:52 +08:00
parent 5f9fdac9f4
commit 9175871678
28 changed files with 618 additions and 582 deletions

View File

@@ -3,9 +3,11 @@
* @Author: ekibun
* @Date: 2020-09-19 10:29:04
* @LastEditors: ekibun
* @LastEditTime: 2020-09-20 15:41:02
* @LastEditTime: 2020-09-21 01:30:41
*/
import 'dart:ffi';
import 'dart:io';
import 'dart:isolate';
import 'package:ffi/ffi.dart';
@@ -24,7 +26,7 @@ class JSProp {
static const WRITABLE = (1 << 1);
static const ENUMERABLE = (1 << 2);
static const C_W_E = (CONFIGURABLE | WRITABLE | ENUMERABLE);
}
}
class JSTag {
static const FIRST = -11; /* first negative tag */
@@ -47,7 +49,11 @@ class JSTag {
static const FLOAT64 = 7;
}
final DynamicLibrary qjsLib = DynamicLibrary.open("test/lib/build/Debug/ffi_library.dll");
final DynamicLibrary qjsLib = Platform.environment['FLUTTER_TEST'] == 'true'
? (Platform.isWindows
? DynamicLibrary.open("test/build/Debug/flutter_qjs.dll")
: DynamicLibrary.process())
: (Platform.isWindows ? DynamicLibrary.open("flutter_qjs_plugin.dll") : DynamicLibrary.process());
/// JSValue *jsEXCEPTION()
final Pointer Function() jsEXCEPTION =
@@ -68,24 +74,28 @@ final Pointer Function(
)>>("jsNewRuntime")
.asFunction();
typedef JSChannel = Pointer Function(Pointer ctx, String method, Pointer argv);
typedef JSChannel = Pointer Function(Pointer ctx, Pointer method, Pointer argv);
class RuntimeOpaque {
JSChannel channel;
List<JSRef> ref = List();
ReceivePort port;
Pointer Function(Future) futureToPromise;
Future Function(Pointer) promsieToFuture;
}
final Map<Pointer, RuntimeOpaque> runtimeOpaques = Map();
Pointer channelDispacher(Pointer ctx, Pointer<Utf8> method, Pointer argv) {
return runtimeOpaques[jsGetRuntime(ctx)].channel(ctx, Utf8.fromUtf8(method), argv);
Pointer channelDispacher(Pointer ctx, Pointer method, Pointer argv) {
return runtimeOpaques[jsGetRuntime(ctx)].channel(ctx, method, argv);
}
Pointer jsNewRuntime(
JSChannel callback,
ReceivePort port,
) {
var rt = _jsNewRuntime(Pointer.fromFunction(channelDispacher));
runtimeOpaques[rt] = RuntimeOpaque()..channel = callback;
runtimeOpaques[rt] = RuntimeOpaque()..channel = callback..port = port;
return rt;
}
@@ -173,6 +183,7 @@ Pointer jsEval(
var val = _jsEval(ctx, utf8input, Utf8.strlen(utf8input), utf8filename, evalFlags);
free(utf8input);
free(utf8filename);
runtimeOpaques[jsGetRuntime(ctx)].port.sendPort.send('eval');
return val;
}
@@ -503,23 +514,18 @@ final Pointer Function(
/// int jsDefinePropertyValue(JSContext *ctx, JSValueConst *this_obj,
/// JSAtom prop, JSValue *val, int flags)
final int Function(
Pointer ctx,
Pointer thisObj,
int prop,
Pointer val,
int flag
) jsDefinePropertyValue = qjsLib
.lookup<
NativeFunction<
Int32 Function(
Pointer,
Pointer,
Uint32,
Pointer,
Int32,
)>>("jsDefinePropertyValue")
.asFunction();
final int Function(Pointer ctx, Pointer thisObj, int prop, Pointer val, int flag)
jsDefinePropertyValue = qjsLib
.lookup<
NativeFunction<
Int32 Function(
Pointer,
Pointer,
Uint32,
Pointer,
Int32,
)>>("jsDefinePropertyValue")
.asFunction();
/// void jsFreeAtom(JSContext *ctx, JSAtom v)
final Pointer Function(
@@ -592,3 +598,102 @@ final int Function(
Int32,
)>>("jsPropertyEnumGetAtom")
.asFunction();
/// uint32_t sizeOfJSValue()
final int Function() _sizeOfJSValue =
qjsLib.lookup<NativeFunction<Uint32 Function()>>("sizeOfJSValue").asFunction();
final sizeOfJSValue = _sizeOfJSValue();
/// void setJSValueList(JSValue *list, int i, JSValue *val)
final void Function(
Pointer list,
int i,
Pointer val,
) setJSValueList = qjsLib
.lookup<
NativeFunction<
Void Function(
Pointer,
Uint32,
Pointer,
)>>("setJSValueList")
.asFunction();
/// JSValue *jsCall(JSContext *ctx, JSValueConst *func_obj, JSValueConst *this_obj,
/// int argc, JSValueConst *argv)
final Pointer Function(
Pointer ctx,
Pointer funcObj,
Pointer thisObj,
int argc,
Pointer argv,
) _jsCall = qjsLib
.lookup<
NativeFunction<
Pointer Function(
Pointer,
Pointer,
Pointer,
Int32,
Pointer,
)>>("jsCall")
.asFunction();
Pointer jsCall(
Pointer ctx,
Pointer funcObj,
Pointer thisObj,
List<Pointer> argv,
) {
Pointer jsArgs = allocate<Uint8>(count: argv.length > 0 ? sizeOfJSValue * argv.length : 1);
for (int i = 0; i < argv.length; ++i) {
Pointer jsArg = argv[i];
setJSValueList(jsArgs, i, jsArg);
}
Pointer func1 = jsDupValue(ctx, funcObj);
Pointer _thisObj = thisObj ?? jsUNDEFINED();
Pointer jsRet = _jsCall(ctx, funcObj, _thisObj, argv.length, jsArgs);
if (thisObj == null) {
jsFreeValue(ctx, _thisObj);
deleteJSValue(_thisObj);
}
jsFreeValue(ctx, func1);
deleteJSValue(func1);
free(jsArgs);
runtimeOpaques[jsGetRuntime(ctx)].port.sendPort.send('call');
return jsRet;
}
/// int jsIsException(JSValueConst *val)
final int Function(
Pointer val,
) jsIsException = qjsLib
.lookup<
NativeFunction<
Int32 Function(
Pointer,
)>>("jsIsException")
.asFunction();
/// JSValue *jsGetException(JSContext *ctx)
final Pointer Function(
Pointer ctx,
) jsGetException = qjsLib
.lookup<
NativeFunction<
Pointer Function(
Pointer,
)>>("jsGetException")
.asFunction();
/// int jsExecutePendingJob(JSRuntime *rt)
final int Function(
Pointer ctx,
) jsExecutePendingJob = qjsLib
.lookup<
NativeFunction<
Int32 Function(
Pointer,
)>>("jsExecutePendingJob")
.asFunction();

View File

@@ -3,131 +3,106 @@
* @Author: ekibun
* @Date: 2020-08-08 08:29:09
* @LastEditors: ekibun
* @LastEditTime: 2020-09-06 13:03:56
* @LastEditTime: 2020-09-21 01:36:30
*/
import 'dart:async';
import 'dart:io';
import 'package:flutter/services.dart';
import 'dart:ffi';
import 'dart:isolate';
import 'package:ffi/ffi.dart';
import 'package:flutter_qjs/ffi.dart';
import 'package:flutter_qjs/wrapper.dart';
/// Handle function to manage js call with `dart(method, ...args)` function.
typedef JsMethodHandler = Future<dynamic> Function(String method, List args);
typedef JsMethodHandler = dynamic Function(String method, List args);
/// Handle function to manage js module.
typedef JsModuleHandler = Future<String> Function(String name);
typedef JsModuleHandler = String Function(String name);
/// return this in [JsMethodHandler] to mark method not implemented.
class JsMethodHandlerNotImplement {}
/// FlutterJs instance.
/// Each [FlutterQjs] object creates a new thread that runs a simple js loop.
/// Make sure call `destroy` to terminate thread and release memory when you don't need it.
class FlutterQjs {
dynamic _engine;
dynamic get pointer => _engine;
Pointer _rt;
Pointer _ctx;
ReceivePort port = ReceivePort();
JsMethodHandler methodHandler;
JsModuleHandler moduleHandler;
_ensureEngine() async {
if (_engine == null) {
_engine = await _FlutterJs.instance._channel.invokeMethod("createEngine");
}
_ensureEngine() {
if (_rt != null) return;
_rt = jsNewRuntime((ctx, method, argv) {
if (method.address != 0) {
var argvs = jsToDart(ctx, argv);
if (methodHandler == null) throw Exception("No MethodHandler");
return dartToJs(ctx, methodHandler(Utf8.fromUtf8(method.cast<Utf8>()), argvs));
}
if (moduleHandler == null) throw Exception("No ModuleHandler");
var ret = Utf8.toUtf8(moduleHandler(Utf8.fromUtf8(argv.cast<Utf8>())));
Future.microtask(() {
free(ret);
});
return ret;
}, port);
_ctx = jsNewContextWithPromsieWrapper(_rt);
}
/// Set a handler to manage js call with `dart(method, ...args)` function.
setMethodHandler(JsMethodHandler handler) async {
if (handler == null)
return _FlutterJs.instance._methodHandlers.remove(_engine);
await _ensureEngine();
_FlutterJs.instance._methodHandlers[_engine] = handler;
setMethodHandler(JsMethodHandler handler) {
methodHandler = handler;
}
/// Set a handler to manage js module.
setModuleHandler(JsModuleHandler handler) async {
if (handler == null)
return _FlutterJs.instance._moduleHandlers.remove(_engine);
await _ensureEngine();
_FlutterJs.instance._moduleHandlers[_engine] = handler;
setModuleHandler(JsModuleHandler handler) {
moduleHandler = handler;
}
/// Terminate thread and release memory.
destroy() async {
if (_engine != null) {
await setMethodHandler(null);
await setModuleHandler(null);
var engine = _engine;
_engine = null;
await _FlutterJs.instance._channel.invokeMethod("close", engine);
/// Free Runtime and Context which can be recreate when evaluate again.
recreate() {
if (_rt != null) {
jsFreeContext(_ctx);
jsFreeRuntime(_rt);
}
_rt = null;
_ctx = null;
}
/// Close ReceivePort.
close() {
if (port != null) {
port.close();
recreate();
}
port = null;
}
/// DispatchMessage
Future<void> dispatch() async {
await for (var _ in port) {
while (true) {
int err = jsExecutePendingJob(_rt);
if (err <= 0) {
if (err < 0) print(parseJSException(_ctx));
break;
}
}
List jsPromises = runtimeOpaques[_rt].ref.where((v) => v is JSPromise).toList();
for (JSPromise jsPromise in jsPromises) {
if (jsPromise.checkResolveReject()) {
jsPromise.release();
runtimeOpaques[_rt].ref.remove(jsPromise);
}
}
}
}
/// Evaluate js script.
Future<dynamic> evaluate(String command, String name) async {
await _ensureEngine();
var arguments = {"engine": _engine, "script": command, "name": name};
return _FlutterJs.instance._wrapFunctionArguments(
await _FlutterJs.instance._channel.invokeMethod("evaluate", arguments),
_engine);
}
}
class _FlutterJs {
factory _FlutterJs() => _getInstance();
static _FlutterJs get instance => _getInstance();
static _FlutterJs _instance;
MethodChannel _channel = const MethodChannel('soko.ekibun.flutter_qjs');
Map<dynamic, JsMethodHandler> _methodHandlers =
Map<dynamic, JsMethodHandler>();
Map<dynamic, JsModuleHandler> _moduleHandlers =
Map<dynamic, JsModuleHandler>();
_FlutterJs._internal() {
_channel.setMethodCallHandler((call) async {
var engine = call.arguments["engine"];
var args = call.arguments["args"];
if (args is List) {
if (_methodHandlers[engine] == null) return call.noSuchMethod(null);
var ret = await _methodHandlers[engine](
call.method, _wrapFunctionArguments(args, engine));
if (ret is JsMethodHandlerNotImplement) return call.noSuchMethod(null);
return ret;
} else {
if (_moduleHandlers[engine] == null) return call.noSuchMethod(null);
var ret = await _moduleHandlers[engine](args);
if (ret is JsMethodHandlerNotImplement) return call.noSuchMethod(null);
return ret;
}
});
}
dynamic _wrapFunctionArguments(dynamic val, dynamic engine) {
if (val is List && !(val is List<int>)) {
for (var i = 0; i < val.length; ++i) {
val[i] = _wrapFunctionArguments(val[i], engine);
}
} else if (val is Map) {
// wrap boolean in Android see https://github.com/flutter/flutter/issues/45066
if (Platform.isAndroid && val["__js_boolean__"] != null) {
return val["__js_boolean__"] != 0;
}
if (val["__js_function__"] != null) {
var functionId = val["__js_function__"];
return (List<dynamic> args) async {
var arguments = {
"engine": engine,
"function": functionId,
"arguments": args,
};
return _wrapFunctionArguments(
await _channel.invokeMethod("call", arguments), engine);
};
} else
for (var key in val.keys) {
val[key] = _wrapFunctionArguments(val[key], engine);
}
}
return val;
}
static _FlutterJs _getInstance() {
if (_instance == null) {
_instance = new _FlutterJs._internal();
}
return _instance;
_ensureEngine();
var jsval = jsEval(_ctx, command, name, JSEvalType.GLOBAL);
if (jsIsException(jsval) != 0) {
throw Exception(parseJSException(_ctx));
}
var ret = runtimeOpaques[_rt]?.promsieToFuture(jsval);
jsFreeValue(_ctx, jsval);
deleteJSValue(jsval);
return ret;
}
}

View File

@@ -3,8 +3,9 @@
* @Author: ekibun
* @Date: 2020-09-19 22:07:47
* @LastEditors: ekibun
* @LastEditTime: 2020-09-20 15:41:16
* @LastEditTime: 2020-09-21 01:23:06
*/
import 'dart:async';
import 'dart:ffi';
import 'dart:typed_data';
@@ -12,10 +13,10 @@ import 'package:ffi/ffi.dart';
import 'ffi.dart';
class JSFunction extends JSRef {
class JSRefValue implements JSRef {
Pointer val;
Pointer ctx;
JSFunction(this.ctx, Pointer val) {
JSRefValue(this.ctx, Pointer val) {
Pointer rt = jsGetRuntime(ctx);
this.val = jsDupValue(ctx, val);
runtimeOpaques[rt]?.ref?.add(this);
@@ -26,17 +27,92 @@ class JSFunction extends JSRef {
if (val != null) {
jsFreeValue(ctx, val);
deleteJSValue(val);
val = null;
}
}
@override
noSuchMethod(Invocation invocation) {
return super.noSuchMethod(invocation);
val = null;
ctx = null;
}
}
class JSPromise extends JSRefValue {
Completer completer;
JSPromise(Pointer ctx, Pointer val, this.completer) : super(ctx, val);
@override
void release() {
super.release();
if (!completer.isCompleted) {
completer.completeError("Promise cannot resolve");
}
}
bool checkResolveReject() {
if (val == null || completer.isCompleted) return true;
var status = jsToDart(ctx, val);
if (status["__resolved"] == true) {
completer.complete(status["__value"]);
return true;
}
if (status["__rejected"] == true) {
completer.completeError(status["__error"] ?? "undefined");
return true;
}
return false;
}
}
class JSFunction extends JSRefValue {
JSFunction(Pointer ctx, Pointer val) : super(ctx, val);
@override
noSuchMethod(Invocation invocation) {
if (val == null) return;
List<Pointer> args = invocation.positionalArguments.map((e) => dartToJs(ctx, e)).toList();
Pointer jsRet = jsCall(ctx, val, null, args);
for (Pointer jsArg in args) {
jsFreeValue(ctx, jsArg);
deleteJSValue(jsArg);
}
bool isException = jsIsException(jsRet) != 0;
var ret = jsToDart(ctx, jsRet);
jsFreeValue(ctx, jsRet);
deleteJSValue(jsRet);
if (isException) {
throw Exception(parseJSException(ctx));
}
return ret;
}
}
Pointer jsGetPropertyStr(Pointer ctx, Pointer val, String prop) {
var jsAtomVal = jsNewString(ctx, prop);
var jsAtom = jsValueToAtom(ctx, jsAtomVal);
Pointer jsProp = jsGetProperty(ctx, val, jsAtom);
jsFreeAtom(ctx, jsAtom);
jsFreeValue(ctx, jsAtomVal);
deleteJSValue(jsAtomVal);
return jsProp;
}
String parseJSException(Pointer ctx) {
Pointer e = jsGetException(ctx);
var err = jsToCString(ctx, e);
if (jsValueGetTag(e) == JSTag.OBJECT) {
Pointer stack = jsGetPropertyStr(ctx, e, "stack");
if (jsToBool(ctx, stack) != 0) {
err += '\n' + jsToCString(ctx, stack);
}
jsFreeValue(ctx, stack);
deleteJSValue(stack);
}
jsFreeValue(ctx, e);
deleteJSValue(e);
return err;
}
Pointer dartToJs(Pointer ctx, dynamic val, {Map<dynamic, dynamic> cache}) {
if (val is Future) {
return runtimeOpaques[jsGetRuntime(ctx)]?.futureToPromise(val);
}
if (cache == null) cache = Map();
if (val is bool) return jsNewBool(ctx, val ? 1 : 0);
if (val is int) return jsNewInt64(ctx, val);
@@ -51,7 +127,7 @@ Pointer dartToJs(Pointer ctx, dynamic val, {Map<dynamic, dynamic> cache}) {
return ret;
}
if (cache.containsKey(val)) {
return cache[val];
return jsDupValue(ctx, cache[val]);
}
if (val is JSFunction) {
return jsDupValue(ctx, val.val);
@@ -78,7 +154,7 @@ Pointer dartToJs(Pointer ctx, dynamic val, {Map<dynamic, dynamic> cache}) {
if (val is Map) {
Pointer ret = jsNewObject(ctx);
cache[val] = ret;
for (MapEntry<dynamic, dynamic> entry in val.entries){
for (MapEntry<dynamic, dynamic> entry in val.entries) {
var jsAtomVal = dartToJs(ctx, entry.key, cache: cache);
var jsAtom = jsValueToAtom(ctx, jsAtomVal);
jsDefinePropertyValue(
@@ -125,13 +201,7 @@ dynamic jsToDart(Pointer ctx, Pointer val, {Map<int, dynamic> cache}) {
if (jsIsFunction(ctx, val) != 0) {
return JSFunction(ctx, val);
} else if (jsIsArray(ctx, val) != 0) {
var jsAtomVal = jsNewString(ctx, "length");
var jsAtom = jsValueToAtom(ctx, jsAtomVal);
var jslength = jsGetProperty(ctx, val, jsAtom);
jsFreeAtom(ctx, jsAtom);
jsFreeValue(ctx, jsAtomVal);
deleteJSValue(jsAtomVal);
Pointer jslength = jsGetPropertyStr(ctx, val, "length");
int length = jsToInt64(ctx, jslength);
deleteJSValue(jslength);
List<dynamic> ret = List();
@@ -175,3 +245,70 @@ dynamic jsToDart(Pointer ctx, Pointer val, {Map<int, dynamic> cache}) {
}
return null;
}
Pointer jsNewContextWithPromsieWrapper(Pointer rt) {
var ctx = jsNewContext(rt);
var jsPromiseCtor = jsEval(
ctx,
"""
() => {
const __resolver = {};
const __ret = new Promise((res, rej) => {
__resolver.__res = res;
__resolver.__rej = rej;
});
__ret.__res = __resolver.__res;
__ret.__rej = __resolver.__rej;
return __ret;
}
""",
"<future>",
JSEvalType.GLOBAL);
var promiseCtor = JSRefValue(ctx, jsPromiseCtor);
jsFreeValue(ctx, jsPromiseCtor);
deleteJSValue(jsPromiseCtor);
runtimeOpaques[rt].futureToPromise = (future) {
var ctor = promiseCtor.val;
if (ctor == null) throw Exception("Runtime has been released!");
var jsPromise = jsCall(ctx, ctor, null, List());
var promise = jsToDart(ctx, jsPromise);
future.then((value) {
promise['__res'](value);
}).catchError((err) {
promise['__rej'](err);
});
return jsPromise;
};
var jsPromiseWrapper = jsEval(
ctx,
"""
(value) => {
const __ret = Promise.resolve(value)
.then(v => {
__ret.__value = v;
__ret.__resolved = true;
}).catch(e => {
__ret.__error = e;
__ret.__rejected = true;
});
return __ret;
}
""",
"<future>",
JSEvalType.GLOBAL);
var promiseWrapper = JSRefValue(ctx, jsPromiseWrapper);
jsFreeValue(ctx, jsPromiseWrapper);
deleteJSValue(jsPromiseWrapper);
runtimeOpaques[rt].promsieToFuture = (promise) {
var completer = Completer();
var wrapper = promiseWrapper.val;
if (wrapper == null) completer.completeError(Exception("Runtime has been released!"));
var jsPromise = jsCall(ctx, wrapper, null, [promise]);
runtimeOpaques[rt].ref.add(JSPromise(ctx, jsPromise, completer));
jsFreeValue(ctx, jsPromise);
deleteJSValue(jsPromise);
return completer.future;
};
return ctx;
}