mirror of
https://github.com/wgh136/flutter_qjs.git
synced 2025-09-27 13:27:24 +00:00
windows wrapper
This commit is contained in:
51
cxx/ffi.cpp
51
cxx/ffi.cpp
@@ -3,10 +3,11 @@
|
|||||||
* @Author: ekibun
|
* @Author: ekibun
|
||||||
* @Date: 2020-09-06 18:32:45
|
* @Date: 2020-09-06 18:32:45
|
||||||
* @LastEditors: ekibun
|
* @LastEditors: ekibun
|
||||||
* @LastEditTime: 2020-09-20 15:50:41
|
* @LastEditTime: 2020-09-21 01:41:39
|
||||||
*/
|
*/
|
||||||
#include "quickjs/quickjs.h"
|
#include "quickjs/quickjs.h"
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
#include <future>
|
||||||
|
|
||||||
#ifdef _MSC_VER
|
#ifdef _MSC_VER
|
||||||
#define DLLEXPORT __declspec(dllexport)
|
#define DLLEXPORT __declspec(dllexport)
|
||||||
@@ -16,7 +17,7 @@
|
|||||||
|
|
||||||
extern "C"
|
extern "C"
|
||||||
{
|
{
|
||||||
typedef JSValue *JSChannel(JSContext *ctx, const char *method, JSValueConst *argv);
|
typedef void *JSChannel(JSContext *ctx, const char *method, void *argv);
|
||||||
|
|
||||||
DLLEXPORT JSValue *jsEXCEPTION()
|
DLLEXPORT JSValue *jsEXCEPTION()
|
||||||
{
|
{
|
||||||
@@ -39,11 +40,13 @@ extern "C"
|
|||||||
{
|
{
|
||||||
JSRuntime *rt = JS_GetRuntime(ctx);
|
JSRuntime *rt = JS_GetRuntime(ctx);
|
||||||
JSChannel *channel = (JSChannel *)JS_GetRuntimeOpaque(rt);
|
JSChannel *channel = (JSChannel *)JS_GetRuntimeOpaque(rt);
|
||||||
JSValue val = *channel(ctx, "__load_module__", new JSValue{JS_NewString(ctx, module_name)});
|
const char *str = (char *)channel(ctx, (char *)0, (void *)module_name);
|
||||||
const char *str = JS_ToCString(ctx, val);
|
if (str == 0)
|
||||||
|
{
|
||||||
|
JS_ThrowReferenceError(ctx, "Module Not Found");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
JSValue func_val = JS_Eval(ctx, str, strlen(str), module_name, JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY);
|
JSValue func_val = JS_Eval(ctx, str, strlen(str), module_name, JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY);
|
||||||
JS_FreeCString(ctx, str);
|
|
||||||
JS_FreeValue(ctx, val);
|
|
||||||
if (JS_IsException(func_val))
|
if (JS_IsException(func_val))
|
||||||
return NULL;
|
return NULL;
|
||||||
/* the module is already referenced, so we must free it */
|
/* the module is already referenced, so we must free it */
|
||||||
@@ -58,7 +61,7 @@ extern "C"
|
|||||||
JSChannel *channel = (JSChannel *)JS_GetRuntimeOpaque(rt);
|
JSChannel *channel = (JSChannel *)JS_GetRuntimeOpaque(rt);
|
||||||
const char *str = JS_ToCString(ctx, argv[0]);
|
const char *str = JS_ToCString(ctx, argv[0]);
|
||||||
JS_DupValue(ctx, *(argv + 1));
|
JS_DupValue(ctx, *(argv + 1));
|
||||||
JSValue ret = *channel(ctx, str, argv + 1);
|
JSValue ret = *(JSValue *)channel(ctx, str, argv + 1);
|
||||||
JS_FreeValue(ctx, *(argv + 1));
|
JS_FreeValue(ctx, *(argv + 1));
|
||||||
JS_FreeCString(ctx, str);
|
JS_FreeCString(ctx, str);
|
||||||
return ret;
|
return ret;
|
||||||
@@ -230,7 +233,7 @@ extern "C"
|
|||||||
}
|
}
|
||||||
|
|
||||||
DLLEXPORT int jsDefinePropertyValue(JSContext *ctx, JSValueConst *this_obj,
|
DLLEXPORT int jsDefinePropertyValue(JSContext *ctx, JSValueConst *this_obj,
|
||||||
JSAtom prop, JSValue *val, int flags)
|
JSAtom prop, JSValue *val, int flags)
|
||||||
{
|
{
|
||||||
return JS_DefinePropertyValue(ctx, *this_obj, prop, *val, flags);
|
return JS_DefinePropertyValue(ctx, *this_obj, prop, *val, flags);
|
||||||
}
|
}
|
||||||
@@ -260,4 +263,36 @@ extern "C"
|
|||||||
{
|
{
|
||||||
return ptab[i].atom;
|
return ptab[i].atom;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DLLEXPORT uint32_t sizeOfJSValue()
|
||||||
|
{
|
||||||
|
return sizeof JSValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
DLLEXPORT void setJSValueList(JSValue *list, uint32_t i, JSValue *val)
|
||||||
|
{
|
||||||
|
list[i] = *val;
|
||||||
|
}
|
||||||
|
|
||||||
|
DLLEXPORT JSValue *jsCall(JSContext *ctx, JSValueConst *func_obj, JSValueConst *this_obj,
|
||||||
|
int argc, JSValueConst *argv)
|
||||||
|
{
|
||||||
|
return new JSValue{JS_Call(ctx, *func_obj, *this_obj, argc, argv)};
|
||||||
|
}
|
||||||
|
|
||||||
|
DLLEXPORT int jsIsException(JSValueConst *val)
|
||||||
|
{
|
||||||
|
return JS_IsException(*val);
|
||||||
|
}
|
||||||
|
|
||||||
|
DLLEXPORT JSValue *jsGetException(JSContext *ctx)
|
||||||
|
{
|
||||||
|
return new JSValue{JS_GetException(ctx)};
|
||||||
|
}
|
||||||
|
|
||||||
|
DLLEXPORT int jsExecutePendingJob(JSRuntime *rt)
|
||||||
|
{
|
||||||
|
JSContext *ctx;
|
||||||
|
return JS_ExecutePendingJob(rt, &ctx);
|
||||||
|
}
|
||||||
}
|
}
|
14
example/ios/Flutter/Generated.xcconfig
Normal file
14
example/ios/Flutter/Generated.xcconfig
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// This is a generated file; do not edit or check into version control.
|
||||||
|
FLUTTER_ROOT=D:\flutter
|
||||||
|
FLUTTER_APPLICATION_PATH=D:\project\vscode\neko\plugins\flutter_qjs\example
|
||||||
|
FLUTTER_TARGET=lib\main.dart
|
||||||
|
FLUTTER_BUILD_DIR=build
|
||||||
|
SYMROOT=${SOURCE_ROOT}/../build\ios
|
||||||
|
OTHER_LDFLAGS=$(inherited) -framework Flutter
|
||||||
|
FLUTTER_FRAMEWORK_DIR=D:\flutter\bin\cache\artifacts\engine\ios
|
||||||
|
FLUTTER_BUILD_NAME=1.0.0
|
||||||
|
FLUTTER_BUILD_NUMBER=1
|
||||||
|
DART_OBFUSCATION=false
|
||||||
|
TRACK_WIDGET_CREATION=false
|
||||||
|
TREE_SHAKE_ICONS=false
|
||||||
|
PACKAGE_CONFIG=.packages
|
15
example/ios/Flutter/flutter_export_environment.sh
Normal file
15
example/ios/Flutter/flutter_export_environment.sh
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# This is a generated file; do not edit or check into version control.
|
||||||
|
export "FLUTTER_ROOT=D:\flutter"
|
||||||
|
export "FLUTTER_APPLICATION_PATH=D:\project\vscode\neko\plugins\flutter_qjs\example"
|
||||||
|
export "FLUTTER_TARGET=lib\main.dart"
|
||||||
|
export "FLUTTER_BUILD_DIR=build"
|
||||||
|
export "SYMROOT=${SOURCE_ROOT}/../build\ios"
|
||||||
|
export "OTHER_LDFLAGS=$(inherited) -framework Flutter"
|
||||||
|
export "FLUTTER_FRAMEWORK_DIR=D:\flutter\bin\cache\artifacts\engine\ios"
|
||||||
|
export "FLUTTER_BUILD_NAME=1.0.0"
|
||||||
|
export "FLUTTER_BUILD_NUMBER=1"
|
||||||
|
export "DART_OBFUSCATION=false"
|
||||||
|
export "TRACK_WIDGET_CREATION=false"
|
||||||
|
export "TREE_SHAKE_ICONS=false"
|
||||||
|
export "PACKAGE_CONFIG=.packages"
|
17
example/ios/Runner/GeneratedPluginRegistrant.h
Normal file
17
example/ios/Runner/GeneratedPluginRegistrant.h
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
//
|
||||||
|
// Generated file. Do not edit.
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef GeneratedPluginRegistrant_h
|
||||||
|
#define GeneratedPluginRegistrant_h
|
||||||
|
|
||||||
|
#import <Flutter/Flutter.h>
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
@interface GeneratedPluginRegistrant : NSObject
|
||||||
|
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry;
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
|
#endif /* GeneratedPluginRegistrant_h */
|
12
example/ios/Runner/GeneratedPluginRegistrant.m
Normal file
12
example/ios/Runner/GeneratedPluginRegistrant.m
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
//
|
||||||
|
// Generated file. Do not edit.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "GeneratedPluginRegistrant.h"
|
||||||
|
|
||||||
|
@implementation GeneratedPluginRegistrant
|
||||||
|
|
||||||
|
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
@@ -3,7 +3,7 @@
|
|||||||
* @Author: ekibun
|
* @Author: ekibun
|
||||||
* @Date: 2020-08-08 08:16:51
|
* @Date: 2020-08-08 08:16:51
|
||||||
* @LastEditors: ekibun
|
* @LastEditors: ekibun
|
||||||
* @LastEditTime: 2020-09-06 19:44:32
|
* @LastEditTime: 2020-09-21 01:20:17
|
||||||
*/
|
*/
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
@@ -53,7 +53,7 @@ class _TestPageState extends State<TestPage> {
|
|||||||
_createEngine() async {
|
_createEngine() async {
|
||||||
if (engine != null) return;
|
if (engine != null) return;
|
||||||
engine = FlutterQjs();
|
engine = FlutterQjs();
|
||||||
await engine.setMethodHandler((String method, List arg) async {
|
engine.setMethodHandler((String method, List arg) async {
|
||||||
switch (method) {
|
switch (method) {
|
||||||
case "http":
|
case "http":
|
||||||
Response response = await Dio().get(arg[0]);
|
Response response = await Dio().get(arg[0]);
|
||||||
@@ -72,14 +72,16 @@ class _TestPageState extends State<TestPage> {
|
|||||||
Float32List(2)
|
Float32List(2)
|
||||||
]);
|
]);
|
||||||
default:
|
default:
|
||||||
return JsMethodHandlerNotImplement();
|
throw Exception("NotImplement");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
await engine.setModuleHandler((String module) async {
|
engine.setModuleHandler((String module) {
|
||||||
if (module == "test") return "export default '${new DateTime.now()}'";
|
if (module == "test") return "export default '${new DateTime.now()}'";
|
||||||
return await rootBundle.loadString(
|
return "";
|
||||||
"js/" + module.replaceFirst(new RegExp(r".js$"), "") + ".js");
|
// return await rootBundle.loadString(
|
||||||
|
// "js/" + module.replaceFirst(new RegExp(r".js$"), "") + ".js");
|
||||||
});
|
});
|
||||||
|
engine.dispatch();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -119,7 +121,7 @@ class _TestPageState extends State<TestPage> {
|
|||||||
child: Text("close engine"),
|
child: Text("close engine"),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
if (engine == null) return;
|
if (engine == null) return;
|
||||||
await engine.destroy();
|
await engine.close();
|
||||||
engine = null;
|
engine = null;
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
@@ -1 +0,0 @@
|
|||||||
6
|
|
@@ -7,12 +7,12 @@ add_executable(${BINARY_NAME} WIN32
|
|||||||
"run_loop.cpp"
|
"run_loop.cpp"
|
||||||
"utils.cpp"
|
"utils.cpp"
|
||||||
"win32_window.cpp"
|
"win32_window.cpp"
|
||||||
"window_configuration.cpp"
|
|
||||||
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
|
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
|
||||||
"Runner.rc"
|
"Runner.rc"
|
||||||
"runner.exe.manifest"
|
"runner.exe.manifest"
|
||||||
)
|
)
|
||||||
apply_standard_settings(${BINARY_NAME})
|
apply_standard_settings(${BINARY_NAME})
|
||||||
|
target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
|
||||||
target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
|
target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
|
||||||
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
|
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
|
||||||
add_dependencies(${BINARY_NAME} flutter_assemble)
|
add_dependencies(${BINARY_NAME} flutter_assemble)
|
||||||
|
@@ -54,6 +54,57 @@ END
|
|||||||
// remains consistent on all systems.
|
// remains consistent on all systems.
|
||||||
IDI_APP_ICON ICON "resources\\app_icon.ico"
|
IDI_APP_ICON ICON "resources\\app_icon.ico"
|
||||||
|
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Version
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifdef FLUTTER_BUILD_NUMBER
|
||||||
|
#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER
|
||||||
|
#else
|
||||||
|
#define VERSION_AS_NUMBER 1,0,0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef FLUTTER_BUILD_NAME
|
||||||
|
#define VERSION_AS_STRING #FLUTTER_BUILD_NAME
|
||||||
|
#else
|
||||||
|
#define VERSION_AS_STRING "1.0.0"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
VS_VERSION_INFO VERSIONINFO
|
||||||
|
FILEVERSION VERSION_AS_NUMBER
|
||||||
|
PRODUCTVERSION VERSION_AS_NUMBER
|
||||||
|
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
|
||||||
|
#ifdef _DEBUG
|
||||||
|
FILEFLAGS VS_FF_DEBUG
|
||||||
|
#else
|
||||||
|
FILEFLAGS 0x0L
|
||||||
|
#endif
|
||||||
|
FILEOS VOS__WINDOWS32
|
||||||
|
FILETYPE VFT_APP
|
||||||
|
FILESUBTYPE 0x0L
|
||||||
|
BEGIN
|
||||||
|
BLOCK "StringFileInfo"
|
||||||
|
BEGIN
|
||||||
|
BLOCK "040904e4"
|
||||||
|
BEGIN
|
||||||
|
VALUE "CompanyName", "soko.ekibun" "\0"
|
||||||
|
VALUE "FileDescription", "A new Flutter project." "\0"
|
||||||
|
VALUE "FileVersion", VERSION_AS_STRING "\0"
|
||||||
|
VALUE "InternalName", "example" "\0"
|
||||||
|
VALUE "LegalCopyright", "Copyright (C) 2020 soko.ekibun. All rights reserved." "\0"
|
||||||
|
VALUE "OriginalFilename", "example.exe" "\0"
|
||||||
|
VALUE "ProductName", "example" "\0"
|
||||||
|
VALUE "ProductVersion", VERSION_AS_STRING "\0"
|
||||||
|
END
|
||||||
|
END
|
||||||
|
BLOCK "VarFileInfo"
|
||||||
|
BEGIN
|
||||||
|
VALUE "Translation", 0x409, 1252
|
||||||
|
END
|
||||||
|
END
|
||||||
|
|
||||||
#endif // English (United States) resources
|
#endif // English (United States) resources
|
||||||
/////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@@ -15,22 +15,25 @@ bool FlutterWindow::OnCreate() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The size here is arbitrary since SetChildContent will resize it.
|
RECT frame = GetClientArea();
|
||||||
flutter_controller_ =
|
|
||||||
std::make_unique<flutter::FlutterViewController>(100, 100, project_);
|
// The size here must match the window dimensions to avoid unnecessary surface
|
||||||
|
// creation / destruction in the startup path.
|
||||||
|
flutter_controller_ = std::make_unique<flutter::FlutterViewController>(
|
||||||
|
frame.right - frame.left, frame.bottom - frame.top, project_);
|
||||||
// Ensure that basic setup of the controller was successful.
|
// Ensure that basic setup of the controller was successful.
|
||||||
if (!flutter_controller_->engine() || !flutter_controller_->view()) {
|
if (!flutter_controller_->engine() || !flutter_controller_->view()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
RegisterPlugins(flutter_controller_.get());
|
RegisterPlugins(flutter_controller_->engine());
|
||||||
run_loop_->RegisterFlutterInstance(flutter_controller_.get());
|
run_loop_->RegisterFlutterInstance(flutter_controller_->engine());
|
||||||
SetChildContent(flutter_controller_->view()->GetNativeWindow());
|
SetChildContent(flutter_controller_->view()->GetNativeWindow());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void FlutterWindow::OnDestroy() {
|
void FlutterWindow::OnDestroy() {
|
||||||
if (flutter_controller_) {
|
if (flutter_controller_) {
|
||||||
run_loop_->UnregisterFlutterInstance(flutter_controller_.get());
|
run_loop_->UnregisterFlutterInstance(flutter_controller_->engine());
|
||||||
flutter_controller_ = nullptr;
|
flutter_controller_ = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,5 +53,12 @@ FlutterWindow::MessageHandler(HWND hwnd, UINT const message,
|
|||||||
return *result;
|
return *result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch (message) {
|
||||||
|
case WM_FONTCHANGE:
|
||||||
|
flutter_controller_->engine()->ReloadSystemFonts();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
return Win32Window::MessageHandler(hwnd, message, wparam, lparam);
|
return Win32Window::MessageHandler(hwnd, message, wparam, lparam);
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
#ifndef FLUTTER_WINDOW_H_
|
#ifndef RUNNER_FLUTTER_WINDOW_H_
|
||||||
#define FLUTTER_WINDOW_H_
|
#define RUNNER_FLUTTER_WINDOW_H_
|
||||||
|
|
||||||
#include <flutter/dart_project.h>
|
#include <flutter/dart_project.h>
|
||||||
#include <flutter/flutter_view_controller.h>
|
#include <flutter/flutter_view_controller.h>
|
||||||
@@ -36,4 +36,4 @@ class FlutterWindow : public Win32Window {
|
|||||||
std::unique_ptr<flutter::FlutterViewController> flutter_controller_;
|
std::unique_ptr<flutter::FlutterViewController> flutter_controller_;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // FLUTTER_WINDOW_H_
|
#endif // RUNNER_FLUTTER_WINDOW_H_
|
||||||
|
@@ -5,7 +5,6 @@
|
|||||||
#include "flutter_window.h"
|
#include "flutter_window.h"
|
||||||
#include "run_loop.h"
|
#include "run_loop.h"
|
||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
#include "window_configuration.h"
|
|
||||||
|
|
||||||
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
||||||
_In_ wchar_t *command_line, _In_ int show_command) {
|
_In_ wchar_t *command_line, _In_ int show_command) {
|
||||||
@@ -23,9 +22,9 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
|||||||
|
|
||||||
flutter::DartProject project(L"data");
|
flutter::DartProject project(L"data");
|
||||||
FlutterWindow window(&run_loop, project);
|
FlutterWindow window(&run_loop, project);
|
||||||
Win32Window::Point origin(kFlutterWindowOriginX, kFlutterWindowOriginY);
|
Win32Window::Point origin(10, 10);
|
||||||
Win32Window::Size size(kFlutterWindowWidth, kFlutterWindowHeight);
|
Win32Window::Size size(1280, 720);
|
||||||
if (!window.CreateAndShow(kFlutterWindowTitle, origin, size)) {
|
if (!window.CreateAndShow(L"example", origin, size)) {
|
||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
window.SetQuitOnClose(true);
|
window.SetQuitOnClose(true);
|
||||||
|
@@ -1,9 +1,6 @@
|
|||||||
#include "run_loop.h"
|
#include "run_loop.h"
|
||||||
|
|
||||||
#include <Windows.h>
|
#include <windows.h>
|
||||||
// Don't stomp std::min/std::max
|
|
||||||
#undef max
|
|
||||||
#undef min
|
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
@@ -47,20 +44,19 @@ void RunLoop::Run() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void RunLoop::RegisterFlutterInstance(
|
void RunLoop::RegisterFlutterInstance(
|
||||||
flutter::FlutterViewController* flutter_instance) {
|
flutter::FlutterEngine* flutter_instance) {
|
||||||
flutter_instances_.insert(flutter_instance);
|
flutter_instances_.insert(flutter_instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
void RunLoop::UnregisterFlutterInstance(
|
void RunLoop::UnregisterFlutterInstance(
|
||||||
flutter::FlutterViewController* flutter_instance) {
|
flutter::FlutterEngine* flutter_instance) {
|
||||||
flutter_instances_.erase(flutter_instance);
|
flutter_instances_.erase(flutter_instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
RunLoop::TimePoint RunLoop::ProcessFlutterMessages() {
|
RunLoop::TimePoint RunLoop::ProcessFlutterMessages() {
|
||||||
TimePoint next_event_time = TimePoint::max();
|
TimePoint next_event_time = TimePoint::max();
|
||||||
for (auto flutter_controller : flutter_instances_) {
|
for (auto instance : flutter_instances_) {
|
||||||
std::chrono::nanoseconds wait_duration =
|
std::chrono::nanoseconds wait_duration = instance->ProcessMessages();
|
||||||
flutter_controller->ProcessMessages();
|
|
||||||
if (wait_duration != std::chrono::nanoseconds::max()) {
|
if (wait_duration != std::chrono::nanoseconds::max()) {
|
||||||
next_event_time =
|
next_event_time =
|
||||||
std::min(next_event_time, TimePoint::clock::now() + wait_duration);
|
std::min(next_event_time, TimePoint::clock::now() + wait_duration);
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
#ifndef RUN_LOOP_H_
|
#ifndef RUNNER_RUN_LOOP_H_
|
||||||
#define RUN_LOOP_H_
|
#define RUNNER_RUN_LOOP_H_
|
||||||
|
|
||||||
#include <flutter/flutter_view_controller.h>
|
#include <flutter/flutter_engine.h>
|
||||||
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <set>
|
#include <set>
|
||||||
@@ -22,11 +22,11 @@ class RunLoop {
|
|||||||
|
|
||||||
// Registers the given Flutter instance for event servicing.
|
// Registers the given Flutter instance for event servicing.
|
||||||
void RegisterFlutterInstance(
|
void RegisterFlutterInstance(
|
||||||
flutter::FlutterViewController* flutter_instance);
|
flutter::FlutterEngine* flutter_instance);
|
||||||
|
|
||||||
// Unregisters the given Flutter instance from event servicing.
|
// Unregisters the given Flutter instance from event servicing.
|
||||||
void UnregisterFlutterInstance(
|
void UnregisterFlutterInstance(
|
||||||
flutter::FlutterViewController* flutter_instance);
|
flutter::FlutterEngine* flutter_instance);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
using TimePoint = std::chrono::steady_clock::time_point;
|
using TimePoint = std::chrono::steady_clock::time_point;
|
||||||
@@ -34,7 +34,7 @@ class RunLoop {
|
|||||||
// Processes all currently pending messages for registered Flutter instances.
|
// Processes all currently pending messages for registered Flutter instances.
|
||||||
TimePoint ProcessFlutterMessages();
|
TimePoint ProcessFlutterMessages();
|
||||||
|
|
||||||
std::set<flutter::FlutterViewController*> flutter_instances_;
|
std::set<flutter::FlutterEngine*> flutter_instances_;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // RUN_LOOP_H_
|
#endif // RUNNER_RUN_LOOP_H_
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
#ifndef CONSOLE_UTILS_H_
|
#ifndef RUNNER_UTILS_H_
|
||||||
#define CONSOLE_UTILS_H_
|
#define RUNNER_UTILS_H_
|
||||||
|
|
||||||
// Creates a console for the process, and redirects stdout and stderr to
|
// Creates a console for the process, and redirects stdout and stderr to
|
||||||
// it for both the runner and the Flutter library.
|
// it for both the runner and the Flutter library.
|
||||||
void CreateAndAttachConsole();
|
void CreateAndAttachConsole();
|
||||||
|
|
||||||
#endif // CONSOLE_UTILS_H_
|
#endif // RUNNER_UTILS_H_
|
||||||
|
@@ -174,8 +174,7 @@ Win32Window::MessageHandler(HWND hwnd,
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
case WM_SIZE:
|
case WM_SIZE:
|
||||||
RECT rect;
|
RECT rect = GetClientArea();
|
||||||
GetClientRect(hwnd, &rect);
|
|
||||||
if (child_content_ != nullptr) {
|
if (child_content_ != nullptr) {
|
||||||
// Size and position the child window.
|
// Size and position the child window.
|
||||||
MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,
|
MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,
|
||||||
@@ -188,11 +187,6 @@ Win32Window::MessageHandler(HWND hwnd,
|
|||||||
SetFocus(child_content_);
|
SetFocus(child_content_);
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
// Messages that are directly forwarded to embedding.
|
|
||||||
case WM_FONTCHANGE:
|
|
||||||
SendMessage(child_content_, WM_FONTCHANGE, NULL, NULL);
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return DefWindowProc(window_handle_, message, wparam, lparam);
|
return DefWindowProc(window_handle_, message, wparam, lparam);
|
||||||
@@ -218,8 +212,7 @@ Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {
|
|||||||
void Win32Window::SetChildContent(HWND content) {
|
void Win32Window::SetChildContent(HWND content) {
|
||||||
child_content_ = content;
|
child_content_ = content;
|
||||||
SetParent(content, window_handle_);
|
SetParent(content, window_handle_);
|
||||||
RECT frame;
|
RECT frame = GetClientArea();
|
||||||
GetClientRect(window_handle_, &frame);
|
|
||||||
|
|
||||||
MoveWindow(content, frame.left, frame.top, frame.right - frame.left,
|
MoveWindow(content, frame.left, frame.top, frame.right - frame.left,
|
||||||
frame.bottom - frame.top, true);
|
frame.bottom - frame.top, true);
|
||||||
@@ -227,6 +220,12 @@ void Win32Window::SetChildContent(HWND content) {
|
|||||||
SetFocus(child_content_);
|
SetFocus(child_content_);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RECT Win32Window::GetClientArea() {
|
||||||
|
RECT frame;
|
||||||
|
GetClientRect(window_handle_, &frame);
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
|
||||||
HWND Win32Window::GetHandle() {
|
HWND Win32Window::GetHandle() {
|
||||||
return window_handle_;
|
return window_handle_;
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,7 @@
|
|||||||
#ifndef WIN32_WINDOW_H_
|
#ifndef RUNNER_WIN32_WINDOW_H_
|
||||||
#define WIN32_WINDOW_H_
|
#define RUNNER_WIN32_WINDOW_H_
|
||||||
|
|
||||||
#include <Windows.h>
|
#include <windows.h>
|
||||||
#include <Windowsx.h>
|
|
||||||
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
@@ -52,6 +51,9 @@ class Win32Window {
|
|||||||
// If true, closing this window will quit the application.
|
// If true, closing this window will quit the application.
|
||||||
void SetQuitOnClose(bool quit_on_close);
|
void SetQuitOnClose(bool quit_on_close);
|
||||||
|
|
||||||
|
// Return a RECT representing the bounds of the current client area.
|
||||||
|
RECT GetClientArea();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
// Processes and route salient window messages for mouse handling,
|
// Processes and route salient window messages for mouse handling,
|
||||||
// size change and DPI. Delegates handling of these to member overloads that
|
// size change and DPI. Delegates handling of these to member overloads that
|
||||||
@@ -93,4 +95,4 @@ class Win32Window {
|
|||||||
HWND child_content_ = nullptr;
|
HWND child_content_ = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // WIN32_WINDOW_H_
|
#endif // RUNNER_WIN32_WINDOW_H_
|
||||||
|
@@ -1,7 +0,0 @@
|
|||||||
#include "window_configuration.h"
|
|
||||||
|
|
||||||
const wchar_t* kFlutterWindowTitle = L"example";
|
|
||||||
const unsigned int kFlutterWindowOriginX = 10;
|
|
||||||
const unsigned int kFlutterWindowOriginY = 10;
|
|
||||||
const unsigned int kFlutterWindowWidth = 1280;
|
|
||||||
const unsigned int kFlutterWindowHeight = 720;
|
|
@@ -1,18 +0,0 @@
|
|||||||
#ifndef WINDOW_CONFIGURATION_
|
|
||||||
#define WINDOW_CONFIGURATION_
|
|
||||||
|
|
||||||
// This is a temporary approach to isolate changes that people are likely to
|
|
||||||
// make to main.cpp, where the APIs are still in flux. This will reduce the
|
|
||||||
// need to resolve conflicts or re-create changes slightly differently every
|
|
||||||
// time the Windows Flutter API surface changes.
|
|
||||||
//
|
|
||||||
// Longer term there should be simpler configuration options for common
|
|
||||||
// customizations like this, without requiring native code changes.
|
|
||||||
|
|
||||||
extern const wchar_t* kFlutterWindowTitle;
|
|
||||||
extern const unsigned int kFlutterWindowOriginX;
|
|
||||||
extern const unsigned int kFlutterWindowOriginY;
|
|
||||||
extern const unsigned int kFlutterWindowWidth;
|
|
||||||
extern const unsigned int kFlutterWindowHeight;
|
|
||||||
|
|
||||||
#endif // WINDOW_CONFIGURATION_
|
|
151
lib/ffi.dart
151
lib/ffi.dart
@@ -3,9 +3,11 @@
|
|||||||
* @Author: ekibun
|
* @Author: ekibun
|
||||||
* @Date: 2020-09-19 10:29:04
|
* @Date: 2020-09-19 10:29:04
|
||||||
* @LastEditors: ekibun
|
* @LastEditors: ekibun
|
||||||
* @LastEditTime: 2020-09-20 15:41:02
|
* @LastEditTime: 2020-09-21 01:30:41
|
||||||
*/
|
*/
|
||||||
import 'dart:ffi';
|
import 'dart:ffi';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:isolate';
|
||||||
|
|
||||||
import 'package:ffi/ffi.dart';
|
import 'package:ffi/ffi.dart';
|
||||||
|
|
||||||
@@ -47,7 +49,11 @@ class JSTag {
|
|||||||
static const FLOAT64 = 7;
|
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()
|
/// JSValue *jsEXCEPTION()
|
||||||
final Pointer Function() jsEXCEPTION =
|
final Pointer Function() jsEXCEPTION =
|
||||||
@@ -68,24 +74,28 @@ final Pointer Function(
|
|||||||
)>>("jsNewRuntime")
|
)>>("jsNewRuntime")
|
||||||
.asFunction();
|
.asFunction();
|
||||||
|
|
||||||
typedef JSChannel = Pointer Function(Pointer ctx, String method, Pointer argv);
|
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 = List();
|
||||||
|
ReceivePort port;
|
||||||
|
Pointer Function(Future) futureToPromise;
|
||||||
|
Future Function(Pointer) promsieToFuture;
|
||||||
}
|
}
|
||||||
|
|
||||||
final Map<Pointer, RuntimeOpaque> runtimeOpaques = Map();
|
final Map<Pointer, RuntimeOpaque> runtimeOpaques = Map();
|
||||||
|
|
||||||
Pointer channelDispacher(Pointer ctx, Pointer<Utf8> method, Pointer argv) {
|
Pointer channelDispacher(Pointer ctx, Pointer method, Pointer argv) {
|
||||||
return runtimeOpaques[jsGetRuntime(ctx)].channel(ctx, Utf8.fromUtf8(method), argv);
|
return runtimeOpaques[jsGetRuntime(ctx)].channel(ctx, method, argv);
|
||||||
}
|
}
|
||||||
|
|
||||||
Pointer jsNewRuntime(
|
Pointer jsNewRuntime(
|
||||||
JSChannel callback,
|
JSChannel callback,
|
||||||
|
ReceivePort port,
|
||||||
) {
|
) {
|
||||||
var rt = _jsNewRuntime(Pointer.fromFunction(channelDispacher));
|
var rt = _jsNewRuntime(Pointer.fromFunction(channelDispacher));
|
||||||
runtimeOpaques[rt] = RuntimeOpaque()..channel = callback;
|
runtimeOpaques[rt] = RuntimeOpaque()..channel = callback..port = port;
|
||||||
return rt;
|
return rt;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,6 +183,7 @@ Pointer jsEval(
|
|||||||
var val = _jsEval(ctx, utf8input, Utf8.strlen(utf8input), utf8filename, evalFlags);
|
var val = _jsEval(ctx, utf8input, Utf8.strlen(utf8input), utf8filename, evalFlags);
|
||||||
free(utf8input);
|
free(utf8input);
|
||||||
free(utf8filename);
|
free(utf8filename);
|
||||||
|
runtimeOpaques[jsGetRuntime(ctx)].port.sendPort.send('eval');
|
||||||
return val;
|
return val;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -503,23 +514,18 @@ final Pointer Function(
|
|||||||
|
|
||||||
/// int jsDefinePropertyValue(JSContext *ctx, JSValueConst *this_obj,
|
/// int jsDefinePropertyValue(JSContext *ctx, JSValueConst *this_obj,
|
||||||
/// JSAtom prop, JSValue *val, int flags)
|
/// JSAtom prop, JSValue *val, int flags)
|
||||||
final int Function(
|
final int Function(Pointer ctx, Pointer thisObj, int prop, Pointer val, int flag)
|
||||||
Pointer ctx,
|
jsDefinePropertyValue = qjsLib
|
||||||
Pointer thisObj,
|
.lookup<
|
||||||
int prop,
|
NativeFunction<
|
||||||
Pointer val,
|
Int32 Function(
|
||||||
int flag
|
Pointer,
|
||||||
) jsDefinePropertyValue = qjsLib
|
Pointer,
|
||||||
.lookup<
|
Uint32,
|
||||||
NativeFunction<
|
Pointer,
|
||||||
Int32 Function(
|
Int32,
|
||||||
Pointer,
|
)>>("jsDefinePropertyValue")
|
||||||
Pointer,
|
.asFunction();
|
||||||
Uint32,
|
|
||||||
Pointer,
|
|
||||||
Int32,
|
|
||||||
)>>("jsDefinePropertyValue")
|
|
||||||
.asFunction();
|
|
||||||
|
|
||||||
/// void jsFreeAtom(JSContext *ctx, JSAtom v)
|
/// void jsFreeAtom(JSContext *ctx, JSAtom v)
|
||||||
final Pointer Function(
|
final Pointer Function(
|
||||||
@@ -592,3 +598,102 @@ final int Function(
|
|||||||
Int32,
|
Int32,
|
||||||
)>>("jsPropertyEnumGetAtom")
|
)>>("jsPropertyEnumGetAtom")
|
||||||
.asFunction();
|
.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();
|
@@ -3,131 +3,106 @@
|
|||||||
* @Author: ekibun
|
* @Author: ekibun
|
||||||
* @Date: 2020-08-08 08:29:09
|
* @Date: 2020-08-08 08:29:09
|
||||||
* @LastEditors: ekibun
|
* @LastEditors: ekibun
|
||||||
* @LastEditTime: 2020-09-06 13:03:56
|
* @LastEditTime: 2020-09-21 01:36:30
|
||||||
*/
|
*/
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:ffi';
|
||||||
import 'package:flutter/services.dart';
|
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.
|
/// 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.
|
/// 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 {
|
class FlutterQjs {
|
||||||
dynamic _engine;
|
Pointer _rt;
|
||||||
dynamic get pointer => _engine;
|
Pointer _ctx;
|
||||||
|
ReceivePort port = ReceivePort();
|
||||||
|
JsMethodHandler methodHandler;
|
||||||
|
JsModuleHandler moduleHandler;
|
||||||
|
|
||||||
_ensureEngine() async {
|
_ensureEngine() {
|
||||||
if (_engine == null) {
|
if (_rt != null) return;
|
||||||
_engine = await _FlutterJs.instance._channel.invokeMethod("createEngine");
|
_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.
|
/// Set a handler to manage js call with `dart(method, ...args)` function.
|
||||||
setMethodHandler(JsMethodHandler handler) async {
|
setMethodHandler(JsMethodHandler handler) {
|
||||||
if (handler == null)
|
methodHandler = handler;
|
||||||
return _FlutterJs.instance._methodHandlers.remove(_engine);
|
|
||||||
await _ensureEngine();
|
|
||||||
_FlutterJs.instance._methodHandlers[_engine] = handler;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set a handler to manage js module.
|
/// Set a handler to manage js module.
|
||||||
setModuleHandler(JsModuleHandler handler) async {
|
setModuleHandler(JsModuleHandler handler) {
|
||||||
if (handler == null)
|
moduleHandler = handler;
|
||||||
return _FlutterJs.instance._moduleHandlers.remove(_engine);
|
|
||||||
await _ensureEngine();
|
|
||||||
_FlutterJs.instance._moduleHandlers[_engine] = handler;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Terminate thread and release memory.
|
/// Free Runtime and Context which can be recreate when evaluate again.
|
||||||
destroy() async {
|
recreate() {
|
||||||
if (_engine != null) {
|
if (_rt != null) {
|
||||||
await setMethodHandler(null);
|
jsFreeContext(_ctx);
|
||||||
await setModuleHandler(null);
|
jsFreeRuntime(_rt);
|
||||||
var engine = _engine;
|
}
|
||||||
_engine = null;
|
_rt = null;
|
||||||
await _FlutterJs.instance._channel.invokeMethod("close", engine);
|
_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.
|
/// Evaluate js script.
|
||||||
Future<dynamic> evaluate(String command, String name) async {
|
Future<dynamic> evaluate(String command, String name) async {
|
||||||
await _ensureEngine();
|
_ensureEngine();
|
||||||
var arguments = {"engine": _engine, "script": command, "name": name};
|
var jsval = jsEval(_ctx, command, name, JSEvalType.GLOBAL);
|
||||||
return _FlutterJs.instance._wrapFunctionArguments(
|
if (jsIsException(jsval) != 0) {
|
||||||
await _FlutterJs.instance._channel.invokeMethod("evaluate", arguments),
|
throw Exception(parseJSException(_ctx));
|
||||||
_engine);
|
}
|
||||||
}
|
var ret = runtimeOpaques[_rt]?.promsieToFuture(jsval);
|
||||||
}
|
jsFreeValue(_ctx, jsval);
|
||||||
|
deleteJSValue(jsval);
|
||||||
class _FlutterJs {
|
return ret;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
173
lib/wrapper.dart
173
lib/wrapper.dart
@@ -3,8 +3,9 @@
|
|||||||
* @Author: ekibun
|
* @Author: ekibun
|
||||||
* @Date: 2020-09-19 22:07:47
|
* @Date: 2020-09-19 22:07:47
|
||||||
* @LastEditors: ekibun
|
* @LastEditors: ekibun
|
||||||
* @LastEditTime: 2020-09-20 15:41:16
|
* @LastEditTime: 2020-09-21 01:23:06
|
||||||
*/
|
*/
|
||||||
|
import 'dart:async';
|
||||||
import 'dart:ffi';
|
import 'dart:ffi';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
@@ -12,10 +13,10 @@ import 'package:ffi/ffi.dart';
|
|||||||
|
|
||||||
import 'ffi.dart';
|
import 'ffi.dart';
|
||||||
|
|
||||||
class JSFunction extends JSRef {
|
class JSRefValue implements JSRef {
|
||||||
Pointer val;
|
Pointer val;
|
||||||
Pointer ctx;
|
Pointer ctx;
|
||||||
JSFunction(this.ctx, Pointer val) {
|
JSRefValue(this.ctx, Pointer val) {
|
||||||
Pointer rt = jsGetRuntime(ctx);
|
Pointer rt = jsGetRuntime(ctx);
|
||||||
this.val = jsDupValue(ctx, val);
|
this.val = jsDupValue(ctx, val);
|
||||||
runtimeOpaques[rt]?.ref?.add(this);
|
runtimeOpaques[rt]?.ref?.add(this);
|
||||||
@@ -26,17 +27,92 @@ class JSFunction extends JSRef {
|
|||||||
if (val != null) {
|
if (val != null) {
|
||||||
jsFreeValue(ctx, val);
|
jsFreeValue(ctx, val);
|
||||||
deleteJSValue(val);
|
deleteJSValue(val);
|
||||||
val = null;
|
|
||||||
}
|
}
|
||||||
}
|
val = null;
|
||||||
|
ctx = null;
|
||||||
@override
|
|
||||||
noSuchMethod(Invocation invocation) {
|
|
||||||
return super.noSuchMethod(invocation);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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}) {
|
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 (cache == null) cache = Map();
|
||||||
if (val is bool) return jsNewBool(ctx, val ? 1 : 0);
|
if (val is bool) return jsNewBool(ctx, val ? 1 : 0);
|
||||||
if (val is int) return jsNewInt64(ctx, val);
|
if (val is int) return jsNewInt64(ctx, val);
|
||||||
@@ -51,7 +127,7 @@ Pointer dartToJs(Pointer ctx, dynamic val, {Map<dynamic, dynamic> cache}) {
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
if (cache.containsKey(val)) {
|
if (cache.containsKey(val)) {
|
||||||
return cache[val];
|
return jsDupValue(ctx, cache[val]);
|
||||||
}
|
}
|
||||||
if (val is JSFunction) {
|
if (val is JSFunction) {
|
||||||
return jsDupValue(ctx, val.val);
|
return jsDupValue(ctx, val.val);
|
||||||
@@ -78,7 +154,7 @@ Pointer dartToJs(Pointer ctx, dynamic val, {Map<dynamic, dynamic> cache}) {
|
|||||||
if (val is Map) {
|
if (val is Map) {
|
||||||
Pointer ret = jsNewObject(ctx);
|
Pointer ret = jsNewObject(ctx);
|
||||||
cache[val] = ret;
|
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 jsAtomVal = dartToJs(ctx, entry.key, cache: cache);
|
||||||
var jsAtom = jsValueToAtom(ctx, jsAtomVal);
|
var jsAtom = jsValueToAtom(ctx, jsAtomVal);
|
||||||
jsDefinePropertyValue(
|
jsDefinePropertyValue(
|
||||||
@@ -125,13 +201,7 @@ 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 (jsIsArray(ctx, val) != 0) {
|
} else if (jsIsArray(ctx, val) != 0) {
|
||||||
var jsAtomVal = jsNewString(ctx, "length");
|
Pointer jslength = jsGetPropertyStr(ctx, val, "length");
|
||||||
var jsAtom = jsValueToAtom(ctx, jsAtomVal);
|
|
||||||
var jslength = jsGetProperty(ctx, val, jsAtom);
|
|
||||||
jsFreeAtom(ctx, jsAtom);
|
|
||||||
jsFreeValue(ctx, jsAtomVal);
|
|
||||||
deleteJSValue(jsAtomVal);
|
|
||||||
|
|
||||||
int length = jsToInt64(ctx, jslength);
|
int length = jsToInt64(ctx, jslength);
|
||||||
deleteJSValue(jslength);
|
deleteJSValue(jslength);
|
||||||
List<dynamic> ret = List();
|
List<dynamic> ret = List();
|
||||||
@@ -175,3 +245,70 @@ dynamic jsToDart(Pointer ctx, Pointer val, {Map<int, dynamic> cache}) {
|
|||||||
}
|
}
|
||||||
return null;
|
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;
|
||||||
|
}
|
||||||
|
@@ -1,9 +1,11 @@
|
|||||||
cmake_minimum_required(VERSION 3.7 FATAL_ERROR)
|
cmake_minimum_required(VERSION 3.7 FATAL_ERROR)
|
||||||
project(ffi_library LANGUAGES CXX)
|
project(ffi_library LANGUAGES CXX)
|
||||||
add_library(ffi_library SHARED ${CMAKE_CURRENT_SOURCE_DIR}/../../cxx/ffi.cpp)
|
set(CXX_LIB_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../cxx)
|
||||||
|
|
||||||
|
add_library(flutter_qjs SHARED ${CXX_LIB_DIR}/ffi.cpp)
|
||||||
|
|
||||||
# quickjs
|
# quickjs
|
||||||
set(QUICK_JS_LIB_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../cxx/quickjs)
|
set(QUICK_JS_LIB_DIR ${CXX_LIB_DIR}/quickjs)
|
||||||
file (STRINGS "${QUICK_JS_LIB_DIR}/VERSION" QUICKJS_VERSION)
|
file (STRINGS "${QUICK_JS_LIB_DIR}/VERSION" QUICKJS_VERSION)
|
||||||
add_library(libquickjs STATIC
|
add_library(libquickjs STATIC
|
||||||
${QUICK_JS_LIB_DIR}/cutils.c
|
${QUICK_JS_LIB_DIR}/cutils.c
|
||||||
@@ -16,4 +18,4 @@ project(libquickjs LANGUAGES C)
|
|||||||
target_compile_options(libquickjs PRIVATE "-DCONFIG_VERSION=\"${QUICKJS_VERSION}\"")
|
target_compile_options(libquickjs PRIVATE "-DCONFIG_VERSION=\"${QUICKJS_VERSION}\"")
|
||||||
target_compile_options(libquickjs PRIVATE "-DDUMP_LEAKS")
|
target_compile_options(libquickjs PRIVATE "-DDUMP_LEAKS")
|
||||||
|
|
||||||
target_link_libraries(ffi_library PRIVATE libquickjs)
|
target_link_libraries(flutter_qjs PRIVATE libquickjs)
|
@@ -3,16 +3,14 @@
|
|||||||
* @Author: ekibun
|
* @Author: ekibun
|
||||||
* @Date: 2020-09-06 13:02:46
|
* @Date: 2020-09-06 13:02:46
|
||||||
* @LastEditors: ekibun
|
* @LastEditors: ekibun
|
||||||
* @LastEditTime: 2020-09-20 15:55:50
|
* @LastEditTime: 2020-09-21 01:39:49
|
||||||
*/
|
*/
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter_qjs/flutter_qjs.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
import 'package:flutter_qjs/ffi.dart';
|
|
||||||
import 'package:flutter_qjs/wrapper.dart';
|
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
test('make', () async {
|
test('make', () async {
|
||||||
final utf8Encoding = Encoding.getByName('utf-8');
|
final utf8Encoding = Encoding.getByName('utf-8');
|
||||||
@@ -22,7 +20,7 @@ void main() async {
|
|||||||
var result = Process.runSync(
|
var result = Process.runSync(
|
||||||
cmakePath,
|
cmakePath,
|
||||||
['-S', './', '-B', buildDir],
|
['-S', './', '-B', buildDir],
|
||||||
workingDirectory: 'test/lib',
|
workingDirectory: 'test',
|
||||||
stdoutEncoding: utf8Encoding,
|
stdoutEncoding: utf8Encoding,
|
||||||
stderrEncoding: utf8Encoding,
|
stderrEncoding: utf8Encoding,
|
||||||
);
|
);
|
||||||
@@ -33,7 +31,7 @@ void main() async {
|
|||||||
result = Process.runSync(
|
result = Process.runSync(
|
||||||
cmakePath,
|
cmakePath,
|
||||||
['--build', buildDir, '--verbose'],
|
['--build', buildDir, '--verbose'],
|
||||||
workingDirectory: 'test/lib',
|
workingDirectory: 'test',
|
||||||
stdoutEncoding: utf8Encoding,
|
stdoutEncoding: utf8Encoding,
|
||||||
stderrEncoding: utf8Encoding,
|
stderrEncoding: utf8Encoding,
|
||||||
);
|
);
|
||||||
@@ -42,36 +40,28 @@ void main() async {
|
|||||||
expect(result.exitCode, 0);
|
expect(result.exitCode, 0);
|
||||||
});
|
});
|
||||||
test('jsToDart', () async {
|
test('jsToDart', () async {
|
||||||
final rt = jsNewRuntime((ctx, method, argv) {
|
final qjs = FlutterQjs();
|
||||||
var argvs = jsToDart(ctx, argv);
|
qjs.setMethodHandler((method, args) {
|
||||||
print([method, argvs]);
|
print([method, args]);
|
||||||
return dartToJs(ctx, [
|
return args;
|
||||||
argvs,
|
|
||||||
{
|
|
||||||
[233, 2]: {}
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
final ctx = jsNewContext(rt);
|
qjs.setModuleHandler((name) {
|
||||||
final jsval = jsEval(
|
print(name);
|
||||||
ctx,
|
return "export default '${new DateTime.now()}'";
|
||||||
"""
|
});
|
||||||
|
qjs.evaluate("""
|
||||||
const a = {};
|
const a = {};
|
||||||
a.a = a;
|
a.a = a;
|
||||||
channel('channel', [
|
import("test").then((module) => channel('channel', [
|
||||||
0.1, true, false, 1, "world",
|
(...a)=>`hello \${a}`,
|
||||||
new ArrayBuffer(2),
|
0.1, true, false, 1, "world", module
|
||||||
()=>'hello',
|
]));
|
||||||
a
|
""", "<eval>").then((value) {
|
||||||
]);
|
print(value);
|
||||||
""",
|
});
|
||||||
"<eval>",
|
Future.delayed(Duration(seconds: 5)).then((v) {
|
||||||
JSEvalType.GLOBAL,
|
qjs.close();
|
||||||
);
|
});
|
||||||
print(jsToDart(ctx, jsval));
|
await qjs.dispatch();
|
||||||
jsFreeValue(ctx, jsval);
|
|
||||||
deleteJSValue(jsval);
|
|
||||||
jsFreeContext(ctx);
|
|
||||||
jsFreeRuntime(rt);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +0,0 @@
|
|||||||
cd %~dp0
|
|
||||||
set CMAKE="C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe"
|
|
||||||
set BUILD_DIR="./build"
|
|
||||||
%CMAKE% -S ./ -B %BUILD_DIR%
|
|
||||||
%CMAKE% --build %BUILD_DIR% --verbose
|
|
@@ -1,7 +1,9 @@
|
|||||||
cmake_minimum_required(VERSION 3.15)
|
cmake_minimum_required(VERSION 3.15)
|
||||||
|
|
||||||
|
set(CXX_LIB_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../cxx)
|
||||||
|
|
||||||
# quickjs
|
# quickjs
|
||||||
set(QUICK_JS_LIB_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../cxx/quickjs)
|
set(QUICK_JS_LIB_DIR ${CXX_LIB_DIR}/quickjs)
|
||||||
file (STRINGS "${QUICK_JS_LIB_DIR}/VERSION" QUICKJS_VERSION)
|
file (STRINGS "${QUICK_JS_LIB_DIR}/VERSION" QUICKJS_VERSION)
|
||||||
add_library(libquickjs STATIC
|
add_library(libquickjs STATIC
|
||||||
${QUICK_JS_LIB_DIR}/cutils.c
|
${QUICK_JS_LIB_DIR}/cutils.c
|
||||||
@@ -21,6 +23,7 @@ set(PLUGIN_NAME "${PROJECT_NAME}_plugin")
|
|||||||
|
|
||||||
add_library(${PLUGIN_NAME} SHARED
|
add_library(${PLUGIN_NAME} SHARED
|
||||||
"${PLUGIN_NAME}.cpp"
|
"${PLUGIN_NAME}.cpp"
|
||||||
|
"${CXX_LIB_DIR}/ffi.cpp"
|
||||||
)
|
)
|
||||||
apply_standard_settings(${PLUGIN_NAME})
|
apply_standard_settings(${PLUGIN_NAME})
|
||||||
set_target_properties(${PLUGIN_NAME} PROPERTIES
|
set_target_properties(${PLUGIN_NAME} PROPERTIES
|
||||||
|
@@ -1,154 +0,0 @@
|
|||||||
/*
|
|
||||||
* @Description:
|
|
||||||
* @Author: ekibun
|
|
||||||
* @Date: 2020-08-14 21:45:02
|
|
||||||
* @LastEditors: ekibun
|
|
||||||
* @LastEditTime: 2020-08-25 18:08:45
|
|
||||||
*/
|
|
||||||
#include "../cxx/js_engine.hpp"
|
|
||||||
#include <flutter/standard_method_codec.h>
|
|
||||||
#include <variant>
|
|
||||||
|
|
||||||
namespace qjs
|
|
||||||
{
|
|
||||||
JSValue dartToJs(JSContext *ctx, flutter::EncodableValue val)
|
|
||||||
{
|
|
||||||
if (val.IsNull())
|
|
||||||
return JS_UNDEFINED;
|
|
||||||
if (std::holds_alternative<bool>(val))
|
|
||||||
return JS_NewBool(ctx, std::get<bool>(val));
|
|
||||||
if (std::holds_alternative<int32_t>(val))
|
|
||||||
return JS_NewInt32(ctx, std::get<int32_t>(val));
|
|
||||||
if (std::holds_alternative<int64_t>(val))
|
|
||||||
return JS_NewInt64(ctx, std::get<int64_t>(val));
|
|
||||||
if (std::holds_alternative<double>(val))
|
|
||||||
return JS_NewFloat64(ctx, std::get<double>(val));
|
|
||||||
if (std::holds_alternative<std::string>(val))
|
|
||||||
return JS_NewString(ctx, std::get<std::string>(val).c_str());
|
|
||||||
if (std::holds_alternative<std::vector<uint8_t>>(val))
|
|
||||||
{
|
|
||||||
auto buf = std::get<std::vector<uint8_t>>(val);
|
|
||||||
return JS_NewArrayBufferCopy(ctx, buf.data(), buf.size());
|
|
||||||
}
|
|
||||||
if (std::holds_alternative<std::vector<int32_t>>(val))
|
|
||||||
{
|
|
||||||
auto buf = std::get<std::vector<int32_t>>(val);
|
|
||||||
return JS_NewArrayBufferCopy(ctx, (uint8_t *)buf.data(), buf.size() * 4);
|
|
||||||
}
|
|
||||||
if (std::holds_alternative<std::vector<int64_t>>(val))
|
|
||||||
{
|
|
||||||
auto buf = std::get<std::vector<int64_t>>(val);
|
|
||||||
return JS_NewArrayBufferCopy(ctx, (uint8_t *)buf.data(), buf.size() * 8);
|
|
||||||
}
|
|
||||||
if (std::holds_alternative<std::vector<double>>(val))
|
|
||||||
{
|
|
||||||
auto buf = std::get<std::vector<double>>(val);
|
|
||||||
JSValue array = JS_NewArray(ctx);
|
|
||||||
auto size = (uint32_t)buf.size();
|
|
||||||
for (uint32_t i = 0; i < size; ++i)
|
|
||||||
{
|
|
||||||
auto atom = JS_NewAtomUInt32(ctx, i);
|
|
||||||
JS_DefinePropertyValue(
|
|
||||||
ctx, array, atom, JS_NewFloat64(ctx, buf[i]),
|
|
||||||
JS_PROP_C_W_E);
|
|
||||||
JS_FreeAtom(ctx, atom);
|
|
||||||
}
|
|
||||||
return array;
|
|
||||||
}
|
|
||||||
if (std::holds_alternative<flutter::EncodableList>(val))
|
|
||||||
{
|
|
||||||
auto list = std::get<flutter::EncodableList>(val);
|
|
||||||
JSValue array = JS_NewArray(ctx);
|
|
||||||
auto size = (uint32_t)list.size();
|
|
||||||
for (uint32_t i = 0; i < size; i++)
|
|
||||||
{
|
|
||||||
auto atom = JS_NewAtomUInt32(ctx, i);
|
|
||||||
JS_DefinePropertyValue(
|
|
||||||
ctx, array, atom, dartToJs(ctx, list[i]),
|
|
||||||
JS_PROP_C_W_E);
|
|
||||||
JS_FreeAtom(ctx, atom);
|
|
||||||
}
|
|
||||||
|
|
||||||
return array;
|
|
||||||
}
|
|
||||||
if (std::holds_alternative<flutter::EncodableMap>(val))
|
|
||||||
{
|
|
||||||
auto map = std::get<flutter::EncodableMap>(val);
|
|
||||||
JSValue obj = JS_NewObject(ctx);
|
|
||||||
for (auto iter = map.begin(); iter != map.end(); ++iter)
|
|
||||||
{
|
|
||||||
auto atomvalue = dartToJs(ctx, iter->first);
|
|
||||||
auto atom = JS_ValueToAtom(ctx, atomvalue);
|
|
||||||
JS_DefinePropertyValue(
|
|
||||||
ctx, obj, atom, dartToJs(ctx, iter->second),
|
|
||||||
JS_PROP_C_W_E);
|
|
||||||
JS_FreeAtom(ctx, atom);
|
|
||||||
JS_FreeValue(ctx, atomvalue);
|
|
||||||
}
|
|
||||||
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
return JS_UNDEFINED;
|
|
||||||
}
|
|
||||||
|
|
||||||
flutter::EncodableValue jsToDart(Value val, std::unordered_map<Value, flutter::EncodableValue> cache = std::unordered_map<Value, flutter::EncodableValue>())
|
|
||||||
{
|
|
||||||
int tag = JS_VALUE_GET_TAG(val.v);
|
|
||||||
if (JS_TAG_IS_FLOAT64(tag))
|
|
||||||
return (double)val;
|
|
||||||
switch (tag)
|
|
||||||
{
|
|
||||||
case JS_TAG_BOOL:
|
|
||||||
return (bool)val;
|
|
||||||
case JS_TAG_INT:
|
|
||||||
return (int64_t)val;
|
|
||||||
case JS_TAG_STRING:
|
|
||||||
return (std::string)val;
|
|
||||||
case JS_TAG_OBJECT:
|
|
||||||
{ // ArrayBuffer
|
|
||||||
size_t size;
|
|
||||||
uint8_t *buf = JS_GetArrayBuffer(val.ctx, &size, val.v);
|
|
||||||
if (buf)
|
|
||||||
return (std::vector<uint8_t>(buf, buf + size));
|
|
||||||
}
|
|
||||||
if (cache.find(val) != cache.end())
|
|
||||||
return cache[val];
|
|
||||||
if (JS_IsFunction(val.ctx, val.v))
|
|
||||||
{
|
|
||||||
flutter::EncodableMap retMap;
|
|
||||||
retMap[std::string("__js_function__")] = (int64_t) new JSValue{js_add_ref(val)};
|
|
||||||
return retMap;
|
|
||||||
}
|
|
||||||
else if (JS_IsArray(val.ctx, val.v) > 0)
|
|
||||||
{
|
|
||||||
flutter::EncodableList retList;
|
|
||||||
cache[val] = retList;
|
|
||||||
uint32_t arrlen = (uint32_t)val["length"];
|
|
||||||
for (uint32_t i = 0; i < arrlen; i++)
|
|
||||||
{
|
|
||||||
retList.push_back(jsToDart(val[i], cache));
|
|
||||||
}
|
|
||||||
return retList;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
qjs::JSPropertyEnum *ptab;
|
|
||||||
uint32_t plen;
|
|
||||||
if (JS_GetOwnPropertyNames(val.ctx, &ptab, &plen, val.v, -1))
|
|
||||||
return flutter::EncodableValue();
|
|
||||||
flutter::EncodableMap retMap;
|
|
||||||
cache[val] = retMap;
|
|
||||||
for (uint32_t i = 0; i < plen; i++)
|
|
||||||
{
|
|
||||||
retMap[jsToDart({val.ctx, JS_AtomToValue(val.ctx, ptab[i].atom)}, cache)] =
|
|
||||||
jsToDart({val.ctx, JS_GetProperty(val.ctx, val.v, ptab[i].atom)}, cache);
|
|
||||||
JS_FreeAtom(val.ctx, ptab[i].atom);
|
|
||||||
}
|
|
||||||
js_free(val.ctx, ptab);
|
|
||||||
return retMap;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return flutter::EncodableValue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} // namespace qjs
|
|
@@ -1,14 +1,16 @@
|
|||||||
|
/*
|
||||||
|
* @Description: empty plugin
|
||||||
|
* @Author: ekibun
|
||||||
|
* @Date: 2020-08-25 21:09:20
|
||||||
|
* @LastEditors: ekibun
|
||||||
|
* @LastEditTime: 2020-09-20 16:00:15
|
||||||
|
*/
|
||||||
#include "include/flutter_qjs/flutter_qjs_plugin.h"
|
#include "include/flutter_qjs/flutter_qjs_plugin.h"
|
||||||
|
|
||||||
// This must be included before many other Windows headers.
|
// This must be included before many other Windows headers.
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
#include <flutter/method_channel.h>
|
|
||||||
#include <flutter/plugin_registrar_windows.h>
|
#include <flutter/plugin_registrar_windows.h>
|
||||||
#include <flutter/standard_method_codec.h>
|
|
||||||
#include <flutter/method_result_functions.h>
|
|
||||||
|
|
||||||
#include "dart_js_wrapper.hpp"
|
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
@@ -21,160 +23,15 @@ namespace
|
|||||||
FlutterQjsPlugin();
|
FlutterQjsPlugin();
|
||||||
|
|
||||||
virtual ~FlutterQjsPlugin();
|
virtual ~FlutterQjsPlugin();
|
||||||
|
|
||||||
private:
|
|
||||||
// Called when a method is called on this plugin's channel from Dart.
|
|
||||||
void HandleMethodCall(
|
|
||||||
const flutter::MethodCall<flutter::EncodableValue> &method_call,
|
|
||||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
std::shared_ptr<flutter::MethodChannel<flutter::EncodableValue>> channel;
|
|
||||||
std::promise<qjs::JSFutureReturn> *invokeChannelMethod(std::string name, qjs::Value args, qjs::Engine *engine)
|
|
||||||
{
|
|
||||||
auto promise = new std::promise<qjs::JSFutureReturn>();
|
|
||||||
auto map = new flutter::EncodableMap();
|
|
||||||
(*map)[std::string("engine")] = (int64_t)engine;
|
|
||||||
(*map)[std::string("args")] = qjs::jsToDart(args);
|
|
||||||
channel->InvokeMethod(
|
|
||||||
name,
|
|
||||||
std::make_unique<flutter::EncodableValue>(*map),
|
|
||||||
std::make_unique<flutter::MethodResultFunctions<flutter::EncodableValue>>(
|
|
||||||
(flutter::ResultHandlerSuccess<flutter::EncodableValue>)[promise](
|
|
||||||
const flutter::EncodableValue *result) {
|
|
||||||
promise->set_value((qjs::JSFutureReturn)[result = result ? *result : flutter::EncodableValue()](qjs::JSContext * ctx) {
|
|
||||||
qjs::JSValue *ret = new qjs::JSValue{qjs::dartToJs(ctx, result)};
|
|
||||||
return qjs::JSOSFutureArgv{1, ret};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
(flutter::ResultHandlerError<flutter::EncodableValue>)[promise](
|
|
||||||
const std::string &error_code,
|
|
||||||
const std::string &error_message,
|
|
||||||
const flutter::EncodableValue *error_details) {
|
|
||||||
promise->set_value((qjs::JSFutureReturn)[error_message](qjs::JSContext * ctx) {
|
|
||||||
qjs::JSValue *ret = new qjs::JSValue{JS_NewString(ctx, error_message.c_str())};
|
|
||||||
return qjs::JSOSFutureArgv{-1, ret};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
(flutter::ResultHandlerNotImplemented<flutter::EncodableValue>)[promise]() {
|
|
||||||
promise->set_value((qjs::JSFutureReturn)[](qjs::JSContext * ctx) {
|
|
||||||
qjs::JSValue *ret = new qjs::JSValue{JS_NewString(ctx, "NotImplemented")};
|
|
||||||
return qjs::JSOSFutureArgv{-1, ret};
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
return promise;
|
|
||||||
}
|
|
||||||
|
|
||||||
// static
|
// static
|
||||||
void FlutterQjsPlugin::RegisterWithRegistrar(
|
void FlutterQjsPlugin::RegisterWithRegistrar(
|
||||||
flutter::PluginRegistrarWindows *registrar)
|
flutter::PluginRegistrarWindows *registrar) {}
|
||||||
{
|
|
||||||
channel =
|
|
||||||
std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>(
|
|
||||||
registrar->messenger(), "soko.ekibun.flutter_qjs",
|
|
||||||
&flutter::StandardMethodCodec::GetInstance());
|
|
||||||
|
|
||||||
auto plugin = std::make_unique<FlutterQjsPlugin>();
|
|
||||||
|
|
||||||
channel->SetMethodCallHandler(
|
|
||||||
[plugin_pointer = plugin.get()](const auto &call, auto result) {
|
|
||||||
plugin_pointer->HandleMethodCall(call, std::move(result));
|
|
||||||
});
|
|
||||||
|
|
||||||
registrar->AddPlugin(std::move(plugin));
|
|
||||||
}
|
|
||||||
|
|
||||||
FlutterQjsPlugin::FlutterQjsPlugin() {}
|
FlutterQjsPlugin::FlutterQjsPlugin() {}
|
||||||
|
|
||||||
FlutterQjsPlugin::~FlutterQjsPlugin() {}
|
FlutterQjsPlugin::~FlutterQjsPlugin() {}
|
||||||
|
|
||||||
const flutter::EncodableValue &ValueOrNull(const flutter::EncodableMap &map, const char *key)
|
|
||||||
{
|
|
||||||
static flutter::EncodableValue null_value;
|
|
||||||
auto it = map.find(flutter::EncodableValue(key));
|
|
||||||
if (it == map.end())
|
|
||||||
{
|
|
||||||
return null_value;
|
|
||||||
}
|
|
||||||
return it->second;
|
|
||||||
}
|
|
||||||
|
|
||||||
void FlutterQjsPlugin::HandleMethodCall(
|
|
||||||
const flutter::MethodCall<flutter::EncodableValue> &method_call,
|
|
||||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result)
|
|
||||||
{
|
|
||||||
// Replace "getPlatformVersion" check with your plugin's method.
|
|
||||||
// See:
|
|
||||||
// https://github.com/flutter/engine/tree/master/shell/platform/common/cpp/client_wrapper/include/flutter
|
|
||||||
// and
|
|
||||||
// https://github.com/flutter/engine/tree/master/shell/platform/glfw/client_wrapper/include/flutter
|
|
||||||
// for the relevant Flutter APIs.
|
|
||||||
if (method_call.method_name().compare("createEngine") == 0)
|
|
||||||
{
|
|
||||||
qjs::Engine *engine = new qjs::Engine(invokeChannelMethod);
|
|
||||||
flutter::EncodableValue response = (int64_t)engine;
|
|
||||||
result->Success(&response);
|
|
||||||
}
|
|
||||||
else if (method_call.method_name().compare("evaluate") == 0)
|
|
||||||
{
|
|
||||||
flutter::EncodableMap args = *std::get_if<flutter::EncodableMap>(method_call.arguments());
|
|
||||||
qjs::Engine *engine = (qjs::Engine *)std::get<int64_t>(ValueOrNull(args, "engine"));
|
|
||||||
std::string script = std::get<std::string>(ValueOrNull(args, "script"));
|
|
||||||
std::string name = std::get<std::string>(ValueOrNull(args, "name"));
|
|
||||||
std::shared_ptr<flutter::MethodResult<flutter::EncodableValue>> presult = std::move(result);
|
|
||||||
engine->commit(qjs::EngineTask{
|
|
||||||
[script, name](qjs::Context &ctx) {
|
|
||||||
return ctx.eval(script, name.c_str(), JS_EVAL_TYPE_GLOBAL);
|
|
||||||
},
|
|
||||||
[presult](qjs::Value resolve) {
|
|
||||||
flutter::EncodableValue response = qjs::jsToDart(resolve);
|
|
||||||
presult->Success(&response);
|
|
||||||
},
|
|
||||||
[presult](qjs::Value reject) {
|
|
||||||
presult->Error("FlutterJSException", qjs::getStackTrack(reject));
|
|
||||||
}});
|
|
||||||
}
|
|
||||||
else if (method_call.method_name().compare("call") == 0)
|
|
||||||
{
|
|
||||||
flutter::EncodableMap args = *std::get_if<flutter::EncodableMap>(method_call.arguments());
|
|
||||||
qjs::Engine *engine = (qjs::Engine *)std::get<int64_t>(ValueOrNull(args, "engine"));
|
|
||||||
qjs::JSValue *function = (qjs::JSValue *)std::get<int64_t>(ValueOrNull(args, "function"));
|
|
||||||
flutter::EncodableList arguments = std::get<flutter::EncodableList>(ValueOrNull(args, "arguments"));
|
|
||||||
std::shared_ptr<flutter::MethodResult<flutter::EncodableValue>> presult = std::move(result);
|
|
||||||
engine->commit(qjs::EngineTask{
|
|
||||||
[function, arguments](qjs::Context &ctx) {
|
|
||||||
size_t argscount = arguments.size();
|
|
||||||
qjs::JSValue *callargs = new qjs::JSValue[argscount];
|
|
||||||
for (size_t i = 0; i < argscount; i++)
|
|
||||||
{
|
|
||||||
callargs[i] = qjs::dartToJs(ctx.ctx, arguments[i]);
|
|
||||||
}
|
|
||||||
qjs::JSValue ret = qjs::call_handler(ctx.ctx, *function, (int)argscount, callargs);
|
|
||||||
delete[] callargs;
|
|
||||||
if (qjs::JS_IsException(ret))
|
|
||||||
throw qjs::exception{};
|
|
||||||
return qjs::Value{ctx.ctx, ret};
|
|
||||||
},
|
|
||||||
[presult](qjs::Value resolve) {
|
|
||||||
flutter::EncodableValue response = qjs::jsToDart(resolve);
|
|
||||||
presult->Success(&response);
|
|
||||||
},
|
|
||||||
[presult](qjs::Value reject) {
|
|
||||||
presult->Error("FlutterJSException", qjs::getStackTrack(reject));
|
|
||||||
}});
|
|
||||||
}
|
|
||||||
else if (method_call.method_name().compare("close") == 0)
|
|
||||||
{
|
|
||||||
qjs::Engine *engine = (qjs::Engine *)*std::get_if<int64_t>(method_call.arguments());
|
|
||||||
delete engine;
|
|
||||||
result->Success();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
result->NotImplemented();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void FlutterQjsPluginRegisterWithRegistrar(
|
void FlutterQjsPluginRegisterWithRegistrar(
|
||||||
|
Reference in New Issue
Block a user