mirror of
https://github.com/wgh136/flutter_qjs.git
synced 2025-09-27 21:37:24 +00:00
0.2.0
This commit is contained in:
@@ -6,6 +6,12 @@
|
|||||||
* @LastEditTime: 2020-12-02 11:36:40
|
* @LastEditTime: 2020-12-02 11:36:40
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
## 0.2.0
|
||||||
|
|
||||||
|
* breakdown change with new constructor.
|
||||||
|
* fix make release in ios.
|
||||||
|
* fix crash in wrapping js Promise.
|
||||||
|
|
||||||
## 0.1.4
|
## 0.1.4
|
||||||
|
|
||||||
* fix crash on android x86.
|
* fix crash on android x86.
|
||||||
|
122
README.md
122
README.md
@@ -19,84 +19,79 @@ ES6 module with `import` function is supported and can be managed in dart with `
|
|||||||
|
|
||||||
A global function `channel` is presented to invoke dart function. Data conversion between dart and js are implemented as follow:
|
A global function `channel` is presented to invoke dart function. Data conversion between dart and js are implemented as follow:
|
||||||
|
|
||||||
| dart | js |
|
| dart | js |
|
||||||
| --- | --- |
|
| --------------------------------------------------- | ------------------ |
|
||||||
| Bool | boolean |
|
| Bool | boolean |
|
||||||
| Int | number |
|
| Int | number |
|
||||||
| Double | number |
|
| Double | number |
|
||||||
| String | string |
|
| String | string |
|
||||||
| Uint8List | ArrayBuffer |
|
| Uint8List | ArrayBuffer |
|
||||||
| List | Array |
|
| List | Array |
|
||||||
| Map | Object |
|
| Map | Object |
|
||||||
| JSFunction | function(....args) |
|
| JSFunction(...args) <br> IsolateJSFunction(...args) | function(....args) |
|
||||||
| Future | Promise |
|
| Future | Promise |
|
||||||
|
|
||||||
**notice:** `function` can only be sent from js to dart. `Promise` return by `evaluate` will be automatically tracked and return the resolved data.
|
**notice:** `function` can only be sent from js to dart. `IsolateJSFunction` always returns asynchronously.
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
### Run on main thread
|
### Run on main thread
|
||||||
|
|
||||||
1. Create a `FlutterQjs` object. Call `dispatch` to dispatch event loop.
|
1. Create a `FlutterQjs` object, pass handlers to implement js-dart interaction and resolving modules. For example, you can use `Dio` to implement http in js:
|
||||||
|
|
||||||
```dart
|
```dart
|
||||||
final engine = FlutterQjs();
|
final engine = FlutterQjs(
|
||||||
await engine.dispatch();
|
methodHandler: (String method, List arg) {
|
||||||
|
switch (method) {
|
||||||
|
case "http":
|
||||||
|
return Dio().get(arg[0]).then((response) => response.data);
|
||||||
|
default:
|
||||||
|
throw Exception("No such method");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
moduleHandler: (String module) {
|
||||||
|
if(module == "hello")
|
||||||
|
return "export default (name) => `hello \${name}!`;";
|
||||||
|
throw Exception("Module Not found");
|
||||||
|
},
|
||||||
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Call `setMethodHandler` to implement js-dart interaction. For example, you can use `Dio` to implement http in js:
|
in javascript, `channel` function is equiped to invoke `methodHandler`, make sure the second parameter is a list:
|
||||||
|
|
||||||
```dart
|
|
||||||
await engine.setMethodHandler((String method, List arg) {
|
|
||||||
switch (method) {
|
|
||||||
case "http":
|
|
||||||
return Dio().get(arg[0]).then((response) => response.data);
|
|
||||||
default:
|
|
||||||
throw Exception("No such method");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
and in javascript, call `channel` function to get data, make sure the second parameter is a list:
|
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
channel("http", ["http://example.com/"]);
|
channel("http", ["http://example.com/"]);
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Call `setModuleHandler` to resolve the js module.
|
`import` function is used to get modules:
|
||||||
|
|
||||||
~~I cannot find a way to convert the sync ffi callback into an async function. So the assets files received by async function `rootBundle.loadString` cannot be used in this version. I will appreciate it if you can provide me a solution to make `ModuleHandler` async.~~
|
|
||||||
|
|
||||||
To use async function in module handler, try [Run on isolate thread](#Run-on-isolate-thread)
|
|
||||||
|
|
||||||
```dart
|
|
||||||
await engine.setModuleHandler((String module) {
|
|
||||||
if(module == "hello") return "export default (name) => `hello \${name}!`;";
|
|
||||||
throw Exception("Module Not found");
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
and in javascript, call `import` function to get module:
|
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
import("hello").then(({default: greet}) => greet("world"));
|
import("hello").then(({default: greet}) => greet("world"));
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Use `evaluate` to run js script:
|
**notice:** To use async function in module handler, try [Run on isolate thread](#Run-on-isolate-thread)
|
||||||
|
|
||||||
|
2. Then call `dispatch` to dispatch event loop.
|
||||||
|
|
||||||
|
```dart
|
||||||
|
engine.dispatch();
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Use `evaluate` to run js script, now you can use it synchronously, or use await to resolve `Promise`:
|
||||||
|
|
||||||
```dart
|
```dart
|
||||||
try {
|
try {
|
||||||
print(await engine.evaluate(code ?? '', "<eval>"));
|
print(engine.evaluate(code ?? ''));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print(e.toString());
|
print(e.toString());
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Method `recreate` can destroy quickjs runtime that can be recreated again if you call `evaluate`, `recreat` can be used to reset the module cache. Call `close` to stop `dispatch` when you do not need it.
|
1. Method `close` can destroy quickjs runtime that can be recreated again if you call `evaluate`.
|
||||||
|
|
||||||
### Run on isolate thread
|
### Run on isolate thread
|
||||||
|
|
||||||
1. Create a `IsolateQjs` object, pass a handler to implement js-dart interaction. The handler is used in isolate, so the function must be a top-level function or a static method.
|
1. Create a `IsolateQjs` object, pass handlers to implement js-dart interaction and resolving modules. The `methodHandler` is used in isolate, so **the handler function must be a top-level function or a static method**. Async function such as `rootBundle.loadString` can be used now to get module:
|
||||||
|
|
||||||
```dart
|
```dart
|
||||||
dynamic methodHandler(String method, List arg) {
|
dynamic methodHandler(String method, List arg) {
|
||||||
@@ -107,32 +102,17 @@ dynamic methodHandler(String method, List arg) {
|
|||||||
throw Exception("No such method");
|
throw Exception("No such method");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final engine = IsolateQjs(methodHandler);
|
final engine = IsolateQjs(
|
||||||
|
methodHandler: methodHandler,
|
||||||
|
moduleHandler: (String module) async {
|
||||||
|
return await rootBundle.loadString(
|
||||||
|
"js/" + module.replaceFirst(new RegExp(r".js$"), "") + ".js");
|
||||||
|
},
|
||||||
|
);
|
||||||
// not need engine.dispatch();
|
// not need engine.dispatch();
|
||||||
```
|
```
|
||||||
|
|
||||||
and in javascript, call `channel` function to get data, make sure the second parameter is a list:
|
2. Same as run on main thread, use `evaluate` to run js script. In this way, `Promise` return by `evaluate` will be automatically tracked and return the resolved data:
|
||||||
|
|
||||||
```javascript
|
|
||||||
channel("http", ["http://example.com/"]);
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Call `setModuleHandler` to resolve the js module. Async function such as `rootBundle.loadString` can be used now to get module. The handler is called in main thread.
|
|
||||||
|
|
||||||
```dart
|
|
||||||
await engine.setModuleHandler((String module) async {
|
|
||||||
return await rootBundle.loadString(
|
|
||||||
"js/" + module.replaceFirst(new RegExp(r".js$"), "") + ".js");
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
and in javascript, call `import` function to get module:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import("hello").then(({default: greet}) => greet("world"));
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Same as run on main thread, use `evaluate` to run js script:
|
|
||||||
|
|
||||||
```dart
|
```dart
|
||||||
try {
|
try {
|
||||||
@@ -142,7 +122,7 @@ try {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Method `close` (same as `recreate` in main thread) can destroy quickjs runtime that can be recreated again if you call `evaluate`.
|
3. Method `close` can destroy quickjs runtime that can be recreated again if you call `evaluate`.
|
||||||
|
|
||||||
[This example](example/lib/main.dart) contains a complete demonstration on how to use this plugin.
|
[This example](example/lib/main.dart) contains a complete demonstration on how to use this plugin.
|
||||||
|
|
||||||
|
15
cxx/ffi.cpp
15
cxx/ffi.cpp
@@ -154,16 +154,18 @@ extern "C"
|
|||||||
return new JSValue(JS_NewObject(ctx));
|
return new JSValue(JS_NewObject(ctx));
|
||||||
}
|
}
|
||||||
|
|
||||||
DLLEXPORT void jsFreeValue(JSContext *ctx, JSValue *v)
|
DLLEXPORT void jsFreeValue(JSContext *ctx, JSValue *v, int32_t free)
|
||||||
{
|
{
|
||||||
JS_FreeValue(ctx, *v);
|
JS_FreeValue(ctx, *v);
|
||||||
delete v;
|
if (free)
|
||||||
|
delete v;
|
||||||
}
|
}
|
||||||
|
|
||||||
DLLEXPORT void jsFreeValueRT(JSRuntime *rt, JSValue *v)
|
DLLEXPORT void jsFreeValueRT(JSRuntime *rt, JSValue *v, int32_t free)
|
||||||
{
|
{
|
||||||
JS_FreeValueRT(rt, *v);
|
JS_FreeValueRT(rt, *v);
|
||||||
delete v;
|
if (free)
|
||||||
|
delete v;
|
||||||
}
|
}
|
||||||
|
|
||||||
DLLEXPORT JSValue *jsDupValue(JSContext *ctx, JSValueConst *v)
|
DLLEXPORT JSValue *jsDupValue(JSContext *ctx, JSValueConst *v)
|
||||||
@@ -215,6 +217,11 @@ extern "C"
|
|||||||
return JS_IsFunction(ctx, *val);
|
return JS_IsFunction(ctx, *val);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DLLEXPORT int32_t jsIsPromise(JSContext *ctx, JSValueConst *val)
|
||||||
|
{
|
||||||
|
return JS_IsPromise(ctx, *val);
|
||||||
|
}
|
||||||
|
|
||||||
DLLEXPORT int32_t jsIsArray(JSContext *ctx, JSValueConst *val)
|
DLLEXPORT int32_t jsIsArray(JSContext *ctx, JSValueConst *val)
|
||||||
{
|
{
|
||||||
return JS_IsArray(ctx, *val);
|
return JS_IsArray(ctx, *val);
|
||||||
|
@@ -51,9 +51,9 @@ extern "C"
|
|||||||
|
|
||||||
DLLEXPORT JSValue *jsNewObject(JSContext *ctx);
|
DLLEXPORT JSValue *jsNewObject(JSContext *ctx);
|
||||||
|
|
||||||
DLLEXPORT void jsFreeValue(JSContext *ctx, JSValue *v);
|
DLLEXPORT void jsFreeValue(JSContext *ctx, JSValue *v, int32_t free);
|
||||||
|
|
||||||
DLLEXPORT void jsFreeValueRT(JSRuntime *rt, JSValue *v);
|
DLLEXPORT void jsFreeValueRT(JSRuntime *rt, JSValue *v, int32_t free);
|
||||||
|
|
||||||
DLLEXPORT JSValue *jsDupValue(JSContext *ctx, JSValueConst *v);
|
DLLEXPORT JSValue *jsDupValue(JSContext *ctx, JSValueConst *v);
|
||||||
|
|
||||||
@@ -73,6 +73,8 @@ extern "C"
|
|||||||
|
|
||||||
DLLEXPORT int32_t jsIsFunction(JSContext *ctx, JSValueConst *val);
|
DLLEXPORT int32_t jsIsFunction(JSContext *ctx, JSValueConst *val);
|
||||||
|
|
||||||
|
DLLEXPORT int32_t jsIsPromise(JSContext *ctx, JSValueConst *val);
|
||||||
|
|
||||||
DLLEXPORT int32_t jsIsArray(JSContext *ctx, JSValueConst *val);
|
DLLEXPORT int32_t jsIsArray(JSContext *ctx, JSValueConst *val);
|
||||||
|
|
||||||
DLLEXPORT JSValue *jsGetProperty(JSContext *ctx, JSValueConst *this_obj,
|
DLLEXPORT JSValue *jsGetProperty(JSContext *ctx, JSValueConst *this_obj,
|
||||||
|
Submodule cxx/quickjs updated: 5143636b2d...fc5dca513b
@@ -75,12 +75,14 @@ class _TestPageState extends State<TestPage> {
|
|||||||
|
|
||||||
_ensureEngine() {
|
_ensureEngine() {
|
||||||
if (engine != null) return;
|
if (engine != null) return;
|
||||||
engine = IsolateQjs(methodHandler);
|
engine = IsolateQjs(
|
||||||
engine.setModuleHandler((String module) async {
|
methodHandler: methodHandler,
|
||||||
if (module == "test") return "export default '${new DateTime.now()}'";
|
moduleHandler: (String module) async {
|
||||||
return await rootBundle.loadString(
|
if (module == "test") return "export default '${new DateTime.now()}'";
|
||||||
"js/" + module.replaceFirst(new RegExp(r".js$"), "") + ".js");
|
return await rootBundle.loadString(
|
||||||
});
|
"js/" + module.replaceFirst(new RegExp(r".js$"), "") + ".js");
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@@ -82,7 +82,7 @@ packages:
|
|||||||
path: ".."
|
path: ".."
|
||||||
relative: true
|
relative: true
|
||||||
source: path
|
source: path
|
||||||
version: "0.1.4"
|
version: "0.2.0"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
|
47
lib/ffi.dart
47
lib/ffi.dart
@@ -16,7 +16,7 @@ abstract class JSRef {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// JS_Eval() flags
|
/// JS_Eval() flags
|
||||||
class JSEvalType {
|
class JSEvalFlag {
|
||||||
static const GLOBAL = 0 << 0;
|
static const GLOBAL = 0 << 0;
|
||||||
static const MODULE = 1 << 0;
|
static const MODULE = 1 << 0;
|
||||||
}
|
}
|
||||||
@@ -110,9 +110,9 @@ typedef JSChannel = Pointer Function(Pointer ctx, Pointer method, Pointer argv);
|
|||||||
|
|
||||||
class RuntimeOpaque {
|
class RuntimeOpaque {
|
||||||
JSChannel channel;
|
JSChannel channel;
|
||||||
List<JSRef> ref = List();
|
List<JSRef> ref = [];
|
||||||
ReceivePort port;
|
ReceivePort port;
|
||||||
Future Function(Pointer) promsieToFuture;
|
Future Function(Pointer) promiseToFuture;
|
||||||
}
|
}
|
||||||
|
|
||||||
final Map<Pointer, RuntimeOpaque> runtimeOpaques = Map();
|
final Map<Pointer, RuntimeOpaque> runtimeOpaques = Map();
|
||||||
@@ -356,32 +356,52 @@ final Pointer Function(
|
|||||||
)>>("jsNewObject")
|
)>>("jsNewObject")
|
||||||
.asFunction();
|
.asFunction();
|
||||||
|
|
||||||
/// void jsFreeValue(JSContext *ctx, JSValue *val)
|
/// void jsFreeValue(JSContext *ctx, JSValue *val, int32_t free)
|
||||||
final void Function(
|
final void Function(
|
||||||
Pointer ctx,
|
Pointer ctx,
|
||||||
Pointer val,
|
Pointer val,
|
||||||
) jsFreeValue = qjsLib
|
int free,
|
||||||
|
) _jsFreeValue = qjsLib
|
||||||
.lookup<
|
.lookup<
|
||||||
NativeFunction<
|
NativeFunction<
|
||||||
Void Function(
|
Void Function(
|
||||||
Pointer,
|
Pointer,
|
||||||
Pointer,
|
Pointer,
|
||||||
|
Int32,
|
||||||
)>>("jsFreeValue")
|
)>>("jsFreeValue")
|
||||||
.asFunction();
|
.asFunction();
|
||||||
|
|
||||||
/// void jsFreeValueRT(JSRuntime *rt, JSValue *v)
|
void jsFreeValue(
|
||||||
|
Pointer ctx,
|
||||||
|
Pointer val, {
|
||||||
|
bool free = true,
|
||||||
|
}) {
|
||||||
|
_jsFreeValue(ctx, val, free ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// void jsFreeValue(JSRuntime *rt, JSValue *val, int32_t free)
|
||||||
final void Function(
|
final void Function(
|
||||||
Pointer rt,
|
Pointer rt,
|
||||||
Pointer val,
|
Pointer val,
|
||||||
) jsFreeValueRT = qjsLib
|
int free,
|
||||||
|
) _jsFreeValueRT = qjsLib
|
||||||
.lookup<
|
.lookup<
|
||||||
NativeFunction<
|
NativeFunction<
|
||||||
Void Function(
|
Void Function(
|
||||||
Pointer,
|
Pointer,
|
||||||
Pointer,
|
Pointer,
|
||||||
|
Int32,
|
||||||
)>>("jsFreeValueRT")
|
)>>("jsFreeValueRT")
|
||||||
.asFunction();
|
.asFunction();
|
||||||
|
|
||||||
|
void jsFreeValueRT(
|
||||||
|
Pointer rt,
|
||||||
|
Pointer val, {
|
||||||
|
bool free = true,
|
||||||
|
}) {
|
||||||
|
_jsFreeValueRT(rt, val, free ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
/// JSValue *jsDupValue(JSContext *ctx, JSValueConst *v)
|
/// JSValue *jsDupValue(JSContext *ctx, JSValueConst *v)
|
||||||
final Pointer Function(
|
final Pointer Function(
|
||||||
Pointer ctx,
|
Pointer ctx,
|
||||||
@@ -512,6 +532,19 @@ final int Function(
|
|||||||
)>>("jsIsFunction")
|
)>>("jsIsFunction")
|
||||||
.asFunction();
|
.asFunction();
|
||||||
|
|
||||||
|
/// int32_t jsIsPromise(JSContext *ctx, JSValueConst *val)
|
||||||
|
final int Function(
|
||||||
|
Pointer ctx,
|
||||||
|
Pointer val,
|
||||||
|
) jsIsPromise = qjsLib
|
||||||
|
.lookup<
|
||||||
|
NativeFunction<
|
||||||
|
Int32 Function(
|
||||||
|
Pointer,
|
||||||
|
Pointer,
|
||||||
|
)>>("jsIsPromise")
|
||||||
|
.asFunction();
|
||||||
|
|
||||||
/// int32_t jsIsArray(JSContext *ctx, JSValueConst *val)
|
/// int32_t jsIsArray(JSContext *ctx, JSValueConst *val)
|
||||||
final int Function(
|
final int Function(
|
||||||
Pointer ctx,
|
Pointer ctx,
|
||||||
|
@@ -23,9 +23,15 @@ class FlutterQjs {
|
|||||||
Pointer _rt;
|
Pointer _rt;
|
||||||
Pointer _ctx;
|
Pointer _ctx;
|
||||||
ReceivePort port = ReceivePort();
|
ReceivePort port = ReceivePort();
|
||||||
|
|
||||||
|
/// Set a handler to manage js call with `channel(method, args)` function.
|
||||||
JsMethodHandler methodHandler;
|
JsMethodHandler methodHandler;
|
||||||
|
|
||||||
|
/// Set a handler to manage js module.
|
||||||
JsModuleHandler moduleHandler;
|
JsModuleHandler moduleHandler;
|
||||||
|
|
||||||
|
FlutterQjs({this.methodHandler, this.moduleHandler});
|
||||||
|
|
||||||
_ensureEngine() {
|
_ensureEngine() {
|
||||||
if (_rt != null) return;
|
if (_rt != null) return;
|
||||||
_rt = jsNewRuntime((ctx, method, argv) {
|
_rt = jsNewRuntime((ctx, method, argv) {
|
||||||
@@ -61,18 +67,8 @@ class FlutterQjs {
|
|||||||
_ctx = jsNewContextWithPromsieWrapper(_rt);
|
_ctx = jsNewContextWithPromsieWrapper(_rt);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set a handler to manage js call with `channel(method, args)` function.
|
|
||||||
setMethodHandler(JsMethodHandler handler) {
|
|
||||||
methodHandler = handler;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set a handler to manage js module.
|
|
||||||
setModuleHandler(JsModuleHandler handler) {
|
|
||||||
moduleHandler = handler;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Free Runtime and Context which can be recreate when evaluate again.
|
/// Free Runtime and Context which can be recreate when evaluate again.
|
||||||
recreate() {
|
close() {
|
||||||
if (_rt != null) {
|
if (_rt != null) {
|
||||||
jsFreeContext(_ctx);
|
jsFreeContext(_ctx);
|
||||||
jsFreeRuntime(_rt);
|
jsFreeRuntime(_rt);
|
||||||
@@ -81,18 +77,10 @@ class FlutterQjs {
|
|||||||
_ctx = null;
|
_ctx = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Close ReceivePort.
|
|
||||||
close() {
|
|
||||||
if (port != null) {
|
|
||||||
port.close();
|
|
||||||
recreate();
|
|
||||||
}
|
|
||||||
port = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// DispatchMessage
|
/// DispatchMessage
|
||||||
Future<void> dispatch() async {
|
Future<void> dispatch() async {
|
||||||
await for (var _ in port) {
|
await for (var _ in port) {
|
||||||
|
if (_rt == null) continue;
|
||||||
while (true) {
|
while (true) {
|
||||||
int err = jsExecutePendingJob(_rt);
|
int err = jsExecutePendingJob(_rt);
|
||||||
if (err <= 0) {
|
if (err <= 0) {
|
||||||
@@ -116,24 +104,14 @@ class FlutterQjs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Evaluate js script.
|
/// Evaluate js script.
|
||||||
Future<dynamic> evaluate(String command, {String name, int evalFlags}) async {
|
dynamic evaluate(String command, {String name, int evalFlags}) {
|
||||||
_ensureEngine();
|
_ensureEngine();
|
||||||
var jsval =
|
var jsval = jsEval(
|
||||||
jsEval(_ctx, command, name ?? "<eval>", evalFlags ?? JSEvalType.GLOBAL);
|
_ctx,
|
||||||
if (jsIsException(jsval) != 0) {
|
command,
|
||||||
jsFreeValue(_ctx, jsval);
|
name ?? "<eval>",
|
||||||
throw Exception(parseJSException(_ctx));
|
evalFlags ?? JSEvalFlag.GLOBAL,
|
||||||
}
|
);
|
||||||
var ret = runtimeOpaques[_rt]?.promsieToFuture(jsval);
|
|
||||||
jsFreeValue(_ctx, jsval);
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Evaluate js script (Sync).
|
|
||||||
dynamic evaluateSync(String command, {String name, int evalFlags}) {
|
|
||||||
_ensureEngine();
|
|
||||||
var jsval =
|
|
||||||
jsEval(_ctx, command, name ?? "<eval>", evalFlags ?? JSEvalType.GLOBAL);
|
|
||||||
if (jsIsException(jsval) != 0) {
|
if (jsIsException(jsval) != 0) {
|
||||||
jsFreeValue(_ctx, jsval);
|
jsFreeValue(_ctx, jsval);
|
||||||
throw Exception(parseJSException(_ctx));
|
throw Exception(parseJSException(_ctx));
|
||||||
|
@@ -76,8 +76,20 @@ dynamic _encodeData(dynamic data, {Map<dynamic, dynamic> cache}) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (data is Future) {
|
if (data is Future) {
|
||||||
// Not support
|
var futurePort = ReceivePort();
|
||||||
return {};
|
data.then((value) {
|
||||||
|
futurePort.first.then((port) => {
|
||||||
|
(port as SendPort).send({'data': _encodeData(value)})
|
||||||
|
});
|
||||||
|
}, onError: (e, stack) {
|
||||||
|
futurePort.first.then((port) => {
|
||||||
|
(port as SendPort)
|
||||||
|
.send({'error': e.toString() + "\n" + stack.toString()})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
'__js_future_port': futurePort.sendPort,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
@@ -104,6 +116,20 @@ dynamic _decodeData(dynamic data, SendPort port,
|
|||||||
return JSFunction.fromAddress(ctx, val);
|
return JSFunction.fromAddress(ctx, val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (data.containsKey('__js_future_port')) {
|
||||||
|
SendPort port = data['__js_future_port'];
|
||||||
|
var futurePort = ReceivePort();
|
||||||
|
port.send(futurePort.sendPort);
|
||||||
|
var futureCompleter = Completer();
|
||||||
|
futurePort.first.then((value) {
|
||||||
|
if (value['error'] != null) {
|
||||||
|
futureCompleter.completeError(value['error']);
|
||||||
|
} else {
|
||||||
|
futureCompleter.complete(value['data']);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return futureCompleter.future;
|
||||||
|
}
|
||||||
var ret = {};
|
var ret = {};
|
||||||
cache[data] = ret;
|
cache[data] = ret;
|
||||||
for (var entry in data.entries) {
|
for (var entry in data.entries) {
|
||||||
@@ -116,30 +142,31 @@ dynamic _decodeData(dynamic data, SendPort port,
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _runJsIsolate(Map spawnMessage) async {
|
void _runJsIsolate(Map spawnMessage) async {
|
||||||
var qjs = FlutterQjs();
|
|
||||||
SendPort sendPort = spawnMessage['port'];
|
SendPort sendPort = spawnMessage['port'];
|
||||||
JsMethodHandler methodHandler = spawnMessage['handler'];
|
JsMethodHandler methodHandler = spawnMessage['handler'];
|
||||||
ReceivePort port = ReceivePort();
|
ReceivePort port = ReceivePort();
|
||||||
sendPort.send(port.sendPort);
|
sendPort.send(port.sendPort);
|
||||||
qjs.setMethodHandler(methodHandler);
|
var qjs = FlutterQjs(
|
||||||
qjs.setModuleHandler((name) {
|
methodHandler: methodHandler,
|
||||||
var ptr = allocate<Pointer<Utf8>>();
|
moduleHandler: (name) {
|
||||||
ptr.value = Pointer.fromAddress(0);
|
var ptr = allocate<Pointer<Utf8>>();
|
||||||
sendPort.send({
|
ptr.value = Pointer.fromAddress(0);
|
||||||
'type': 'module',
|
sendPort.send({
|
||||||
'name': name,
|
'type': 'module',
|
||||||
'ptr': ptr.address,
|
'name': name,
|
||||||
});
|
'ptr': ptr.address,
|
||||||
while (ptr.value.address == 0) sleep(Duration.zero);
|
});
|
||||||
if (ptr.value.address == -1) throw Exception("Module Not found");
|
while (ptr.value.address == 0) sleep(Duration.zero);
|
||||||
var ret = Utf8.fromUtf8(ptr.value);
|
if (ptr.value.address == -1) throw Exception("Module Not found");
|
||||||
sendPort.send({
|
var ret = Utf8.fromUtf8(ptr.value);
|
||||||
'type': 'release',
|
sendPort.send({
|
||||||
'ptr': ptr.value.address,
|
'type': 'release',
|
||||||
});
|
'ptr': ptr.value.address,
|
||||||
free(ptr);
|
});
|
||||||
return ret;
|
free(ptr);
|
||||||
});
|
return ret;
|
||||||
|
},
|
||||||
|
);
|
||||||
qjs.dispatch();
|
qjs.dispatch();
|
||||||
await for (var msg in port) {
|
await for (var msg in port) {
|
||||||
var data;
|
var data;
|
||||||
@@ -160,6 +187,7 @@ void _runJsIsolate(Map spawnMessage) async {
|
|||||||
).invoke(_decodeData(msg['args'], null));
|
).invoke(_decodeData(msg['args'], null));
|
||||||
break;
|
break;
|
||||||
case 'close':
|
case 'close':
|
||||||
|
qjs.port.close();
|
||||||
qjs.close();
|
qjs.close();
|
||||||
port.close();
|
port.close();
|
||||||
break;
|
break;
|
||||||
@@ -182,12 +210,15 @@ typedef JsIsolateSpawn = void Function(SendPort sendPort);
|
|||||||
|
|
||||||
class IsolateQjs {
|
class IsolateQjs {
|
||||||
Future<SendPort> _sendPort;
|
Future<SendPort> _sendPort;
|
||||||
JsMethodHandler _methodHandler;
|
|
||||||
JsAsyncModuleHandler _moduleHandler;
|
|
||||||
|
|
||||||
/// Set a handler to manage js call with `channel(method, args)` function.
|
/// Set a handler to manage js call with `channel(method, args)` function.
|
||||||
/// The function must be a top-level function or a static method
|
/// The function must be a top-level function or a static method
|
||||||
IsolateQjs(this._methodHandler);
|
JsMethodHandler methodHandler;
|
||||||
|
|
||||||
|
/// Set a handler to manage js module.
|
||||||
|
JsAsyncModuleHandler moduleHandler;
|
||||||
|
|
||||||
|
IsolateQjs({this.methodHandler, this.moduleHandler});
|
||||||
|
|
||||||
_ensureEngine() {
|
_ensureEngine() {
|
||||||
if (_sendPort != null) return;
|
if (_sendPort != null) return;
|
||||||
@@ -196,7 +227,7 @@ class IsolateQjs {
|
|||||||
_runJsIsolate,
|
_runJsIsolate,
|
||||||
{
|
{
|
||||||
'port': port.sendPort,
|
'port': port.sendPort,
|
||||||
'handler': _methodHandler,
|
'handler': methodHandler,
|
||||||
},
|
},
|
||||||
errorsAreFatal: true,
|
errorsAreFatal: true,
|
||||||
);
|
);
|
||||||
@@ -210,7 +241,7 @@ class IsolateQjs {
|
|||||||
case 'module':
|
case 'module':
|
||||||
var ptr = Pointer<Pointer>.fromAddress(msg['ptr']);
|
var ptr = Pointer<Pointer>.fromAddress(msg['ptr']);
|
||||||
try {
|
try {
|
||||||
ptr.value = Utf8.toUtf8(await _moduleHandler(msg['name']));
|
ptr.value = Utf8.toUtf8(await moduleHandler(msg['name']));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ptr.value = Pointer.fromAddress(-1);
|
ptr.value = Pointer.fromAddress(-1);
|
||||||
}
|
}
|
||||||
@@ -226,11 +257,6 @@ class IsolateQjs {
|
|||||||
_sendPort = completer.future;
|
_sendPort = completer.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set a handler to manage js module.
|
|
||||||
setModuleHandler(JsAsyncModuleHandler handler) {
|
|
||||||
_moduleHandler = handler;
|
|
||||||
}
|
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
if (_sendPort == null) return;
|
if (_sendPort == null) return;
|
||||||
_sendPort.then((sendPort) {
|
_sendPort.then((sendPort) {
|
||||||
@@ -253,8 +279,9 @@ class IsolateQjs {
|
|||||||
'port': evaluatePort.sendPort,
|
'port': evaluatePort.sendPort,
|
||||||
});
|
});
|
||||||
var result = await evaluatePort.first;
|
var result = await evaluatePort.first;
|
||||||
if (result['error'] == null)
|
if (result['error'] == null){
|
||||||
return _decodeData(result['data'], sendPort);
|
return _decodeData(result['data'], sendPort);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
throw result['error'];
|
throw result['error'];
|
||||||
}
|
}
|
||||||
|
@@ -130,8 +130,8 @@ Pointer dartToJs(Pointer ctx, dynamic val, {Map<dynamic, dynamic> cache}) {
|
|||||||
var ret = jsNewPromiseCapability(ctx, resolvingFunc);
|
var ret = jsNewPromiseCapability(ctx, resolvingFunc);
|
||||||
var res = jsToDart(ctx, resolvingFunc);
|
var res = jsToDart(ctx, resolvingFunc);
|
||||||
var rej = jsToDart(ctx, resolvingFunc2);
|
var rej = jsToDart(ctx, resolvingFunc2);
|
||||||
jsFreeValue(ctx, resolvingFunc);
|
jsFreeValue(ctx, resolvingFunc, free: false);
|
||||||
jsFreeValue(ctx, resolvingFunc2);
|
jsFreeValue(ctx, resolvingFunc2, free: false);
|
||||||
free(resolvingFunc);
|
free(resolvingFunc);
|
||||||
val.then((value) {
|
val.then((value) {
|
||||||
res(value);
|
res(value);
|
||||||
@@ -225,10 +225,12 @@ dynamic jsToDart(Pointer ctx, Pointer val, {Map<int, dynamic> cache}) {
|
|||||||
}
|
}
|
||||||
if (jsIsFunction(ctx, val) != 0) {
|
if (jsIsFunction(ctx, val) != 0) {
|
||||||
return JSFunction(ctx, val);
|
return JSFunction(ctx, val);
|
||||||
|
} else if (jsIsPromise(ctx, val) != 0) {
|
||||||
|
return runtimeOpaques[jsGetRuntime(ctx)]?.promiseToFuture(val);
|
||||||
} else if (jsIsArray(ctx, val) != 0) {
|
} else if (jsIsArray(ctx, val) != 0) {
|
||||||
Pointer jslength = jsGetPropertyStr(ctx, val, "length");
|
Pointer jslength = jsGetPropertyStr(ctx, val, "length");
|
||||||
int length = jsToInt64(ctx, jslength);
|
int length = jsToInt64(ctx, jslength);
|
||||||
List<dynamic> ret = List();
|
List<dynamic> ret = [];
|
||||||
cache[valptr] = ret;
|
cache[valptr] = ret;
|
||||||
for (int i = 0; i < length; ++i) {
|
for (int i = 0; i < length; ++i) {
|
||||||
var jsAtomVal = jsNewInt64(ctx, i);
|
var jsAtomVal = jsNewInt64(ctx, i);
|
||||||
@@ -274,7 +276,8 @@ Pointer jsNewContextWithPromsieWrapper(Pointer rt) {
|
|||||||
ctx,
|
ctx,
|
||||||
"""
|
"""
|
||||||
(value) => {
|
(value) => {
|
||||||
const __ret = Promise.resolve(value)
|
const __ret = {};
|
||||||
|
Promise.resolve(value)
|
||||||
.then(v => {
|
.then(v => {
|
||||||
__ret.__value = v;
|
__ret.__value = v;
|
||||||
__ret.__resolved = true;
|
__ret.__resolved = true;
|
||||||
@@ -286,10 +289,10 @@ Pointer jsNewContextWithPromsieWrapper(Pointer rt) {
|
|||||||
}
|
}
|
||||||
""",
|
""",
|
||||||
"<future>",
|
"<future>",
|
||||||
JSEvalType.GLOBAL);
|
JSEvalFlag.GLOBAL);
|
||||||
var promiseWrapper = JSRefValue(ctx, jsPromiseWrapper);
|
var promiseWrapper = JSRefValue(ctx, jsPromiseWrapper);
|
||||||
jsFreeValue(ctx, jsPromiseWrapper);
|
jsFreeValue(ctx, jsPromiseWrapper);
|
||||||
runtimeOpaques[rt].promsieToFuture = (promise) {
|
runtimeOpaques[rt].promiseToFuture = (promise) {
|
||||||
var completer = Completer();
|
var completer = Completer();
|
||||||
var wrapper = promiseWrapper.val;
|
var wrapper = promiseWrapper.val;
|
||||||
if (wrapper == null)
|
if (wrapper == null)
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
name: flutter_qjs
|
name: flutter_qjs
|
||||||
description: This plugin is a simple js engine for flutter using the `quickjs` project. Plugin currently supports all the platforms except web!
|
description: This plugin is a simple js engine for flutter using the `quickjs` project. Plugin currently supports all the platforms except web!
|
||||||
version: 0.1.4
|
version: 0.2.0
|
||||||
homepage: https://github.com/ekibun/flutter_qjs
|
homepage: https://github.com/ekibun/flutter_qjs
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
|
@@ -5,9 +5,11 @@
|
|||||||
* @LastEditors: ekibun
|
* @LastEditors: ekibun
|
||||||
* @LastEditTime: 2020-10-07 00:11:27
|
* @LastEditTime: 2020-10-07 00:11:27
|
||||||
*/
|
*/
|
||||||
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter_qjs/ffi.dart';
|
||||||
import 'package:flutter_qjs/flutter_qjs.dart';
|
import 'package:flutter_qjs/flutter_qjs.dart';
|
||||||
import 'package:flutter_qjs/isolate.dart';
|
import 'package:flutter_qjs/isolate.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
@@ -16,6 +18,30 @@ dynamic myMethodHandler(method, args) {
|
|||||||
return args;
|
return args;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future testEvaluate(qjs) async {
|
||||||
|
var value = await qjs.evaluate("""
|
||||||
|
const a = {};
|
||||||
|
a.a = a;
|
||||||
|
import('test').then((module) => channel('channel', [
|
||||||
|
(...args)=>`hello \${args}!`, a,
|
||||||
|
Promise.reject('test Promise.reject'), Promise.resolve('test Promise.resolve'),
|
||||||
|
0.1, true, false, 1, "world", module
|
||||||
|
]));
|
||||||
|
""", name: "<eval>");
|
||||||
|
expect(await value[0]('world'), 'hello world!', reason: "js function call");
|
||||||
|
expect(value[1]['a'], value[1], reason: "recursive object");
|
||||||
|
expect(value[2], isInstanceOf<Future>(), reason: "promise object");
|
||||||
|
try {
|
||||||
|
await value[2];
|
||||||
|
throw 'Future not reject';
|
||||||
|
} catch (e) {
|
||||||
|
expect(e, startsWith('test Promise.reject\n'),
|
||||||
|
reason: "promise object reject");
|
||||||
|
}
|
||||||
|
expect(await value[3], 'test Promise.resolve',
|
||||||
|
reason: "promise object resolve");
|
||||||
|
}
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
test('make.windows', () async {
|
test('make.windows', () async {
|
||||||
final utf8Encoding = Encoding.getByName('utf-8');
|
final utf8Encoding = Encoding.getByName('utf-8');
|
||||||
@@ -54,27 +80,56 @@ void main() async {
|
|||||||
stderr.write(result.stderr);
|
stderr.write(result.stderr);
|
||||||
expect(result.exitCode, 0);
|
expect(result.exitCode, 0);
|
||||||
}, testOn: 'mac-os');
|
}, testOn: 'mac-os');
|
||||||
test('jsToDart', () async {
|
test('module', () async {
|
||||||
final qjs = IsolateQjs(myMethodHandler);
|
final qjs = FlutterQjs(
|
||||||
qjs.setModuleHandler((name) async {
|
moduleHandler: (name) {
|
||||||
return "export default '${new DateTime.now()}'";
|
return "export default 'test module'";
|
||||||
});
|
},
|
||||||
var value = await qjs.evaluate("""
|
);
|
||||||
const a = {};
|
qjs.dispatch();
|
||||||
a.a = a;
|
qjs.evaluate('''
|
||||||
import("test").then((module) => channel('channel', [
|
import handlerData from 'test';
|
||||||
(...args)=>`hello \${args}!`, a,
|
export default {
|
||||||
0.1, true, false, 1, "world", module
|
data: handlerData
|
||||||
]));
|
};
|
||||||
""", name: "<eval>");
|
''', name: 'evalModule', evalFlags: JSEvalFlag.MODULE);
|
||||||
expect(value[1]['a'], value[1], reason: "recursive object");
|
var result = await qjs.evaluate('import("evalModule")');
|
||||||
expect(await value[0]('world'), 'hello world!', reason: "js function call");
|
expect(result['default']['data'], 'test module', reason: "eval module");
|
||||||
qjs.close();
|
qjs.close();
|
||||||
});
|
});
|
||||||
|
test('jsToDart', () async {
|
||||||
|
await runZonedGuarded(() async {
|
||||||
|
final qjs = FlutterQjs(
|
||||||
|
methodHandler: myMethodHandler,
|
||||||
|
moduleHandler: (name) {
|
||||||
|
return "export default '${new DateTime.now()}'";
|
||||||
|
},
|
||||||
|
);
|
||||||
|
qjs.dispatch();
|
||||||
|
await testEvaluate(qjs);
|
||||||
|
qjs.close();
|
||||||
|
}, (e, stack) {
|
||||||
|
if (e is TestFailure) throw e;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test('isolate', () async {
|
||||||
|
await runZonedGuarded(() async {
|
||||||
|
final qjs = IsolateQjs(
|
||||||
|
methodHandler: myMethodHandler,
|
||||||
|
moduleHandler: (name) async {
|
||||||
|
return "export default '${new DateTime.now()}'";
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await testEvaluate(qjs);
|
||||||
|
qjs.close();
|
||||||
|
}, (e, stack) {
|
||||||
|
if (e is TestFailure) throw e;
|
||||||
|
});
|
||||||
|
});
|
||||||
test('stack overflow', () async {
|
test('stack overflow', () async {
|
||||||
final qjs = FlutterQjs();
|
final qjs = FlutterQjs();
|
||||||
try {
|
try {
|
||||||
await qjs.evaluate("a=()=>a();a();", name: "<eval>");
|
qjs.evaluate("a=()=>a();a();", name: "<eval>");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
expect(
|
expect(
|
||||||
e.toString(), startsWith('Exception: InternalError: stack overflow'),
|
e.toString(), startsWith('Exception: InternalError: stack overflow'),
|
||||||
|
Reference in New Issue
Block a user