commit 17d2876e36f63b38958fa32573cf0b14fa6f1541 Author: ekibun Date: Sat Aug 8 18:00:21 2020 +0800 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e9dc58d --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +.dart_tool/ + +.packages +.pub/ + +build/ diff --git a/.idea/libraries/Dart_SDK.xml b/.idea/libraries/Dart_SDK.xml new file mode 100644 index 0000000..70af93c --- /dev/null +++ b/.idea/libraries/Dart_SDK.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..4f16f99 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/.idea/runConfigurations/example_lib_main_dart.xml b/.idea/runConfigurations/example_lib_main_dart.xml new file mode 100644 index 0000000..5fd9159 --- /dev/null +++ b/.idea/runConfigurations/example_lib_main_dart.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..e513d54 --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..30cf29b --- /dev/null +++ b/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: f3b7788f7754a51092ae1d677001767960c21910 + channel: master + +project_type: plugin diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..c4d33c2 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,14 @@ +{ + // 使用 IntelliSense 了解相关属性。 + // 悬停以查看现有属性的描述。 + // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Flutter", + "program": "lib/main.dart", + "request": "launch", + "type": "dart" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..ade0a35 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,62 @@ +{ + "files.associations": { + "algorithm": "cpp", + "atomic": "cpp", + "cctype": "cpp", + "chrono": "cpp", + "cmath": "cpp", + "concepts": "cpp", + "condition_variable": "cpp", + "cstddef": "cpp", + "cstdint": "cpp", + "cstdio": "cpp", + "cstdlib": "cpp", + "cstring": "cpp", + "cwchar": "cpp", + "deque": "cpp", + "exception": "cpp", + "resumable": "cpp", + "functional": "cpp", + "future": "cpp", + "initializer_list": "cpp", + "ios": "cpp", + "iosfwd": "cpp", + "iostream": "cpp", + "istream": "cpp", + "iterator": "cpp", + "limits": "cpp", + "list": "cpp", + "map": "cpp", + "memory": "cpp", + "mutex": "cpp", + "new": "cpp", + "ostream": "cpp", + "queue": "cpp", + "ratio": "cpp", + "set": "cpp", + "sstream": "cpp", + "stdexcept": "cpp", + "streambuf": "cpp", + "string": "cpp", + "system_error": "cpp", + "thread": "cpp", + "tuple": "cpp", + "type_traits": "cpp", + "typeinfo": "cpp", + "unordered_map": "cpp", + "utility": "cpp", + "vector": "cpp", + "xfacet": "cpp", + "xhash": "cpp", + "xiosbase": "cpp", + "xlocale": "cpp", + "xlocinfo": "cpp", + "xlocnum": "cpp", + "xmemory": "cpp", + "xstddef": "cpp", + "xstring": "cpp", + "xtr1common": "cpp", + "xtree": "cpp", + "xutility": "cpp" + } +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..41cc7d8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ba75c69 --- /dev/null +++ b/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/README.md b/README.md new file mode 100644 index 0000000..fe10bcc --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# flutter_qjs + +A new flutter plugin project. + +## Getting Started + +This project is a starting point for a Flutter +[plug-in package](https://flutter.dev/developing-packages/), +a specialized package that includes platform-specific implementation code for +Android and/or iOS. + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. + +The plugin project was generated without specifying the `--platforms` flag, no platforms are currently supported. +To add platforms, run `flutter create -t plugin --platforms .` under the same +directory. You can also find a detailed instruction on how to add platforms in the `pubspec.yaml` at https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin-platforms. diff --git a/example/.gitignore b/example/.gitignore new file mode 100644 index 0000000..9d532b1 --- /dev/null +++ b/example/.gitignore @@ -0,0 +1,41 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json diff --git a/example/.metadata b/example/.metadata new file mode 100644 index 0000000..9186930 --- /dev/null +++ b/example/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: f3b7788f7754a51092ae1d677001767960c21910 + channel: master + +project_type: app diff --git a/example/README.md b/example/README.md new file mode 100644 index 0000000..2649690 --- /dev/null +++ b/example/README.md @@ -0,0 +1,16 @@ +# flutter_qjs_example + +Demonstrates how to use the flutter_qjs plugin. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/example/lib/code/editor.dart b/example/lib/code/editor.dart new file mode 100644 index 0000000..b99d493 --- /dev/null +++ b/example/lib/code/editor.dart @@ -0,0 +1,34 @@ +/* + * @Description: + * @Author: ekibun + * @Date: 2020-08-01 13:20:06 + * @LastEditors: ekibun + * @LastEditTime: 2020-08-08 17:52:22 + */ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'highlight.dart'; + +class CodeEditor extends StatefulWidget { + final void Function(String) onChanged; + + const CodeEditor({Key key, this.onChanged}) : super(key: key); + @override + _CodeEditorState createState() => _CodeEditorState(); +} + +class _CodeEditorState extends State { + CodeInputController _controller = CodeInputController(); + + @override + Widget build(BuildContext context) { + return TextField( + autofocus: true, + controller: _controller, + textCapitalization: TextCapitalization.none, + decoration: null, + maxLines: null, + onChanged: this.widget.onChanged, + ); + } +} diff --git a/example/lib/code/highlight.dart b/example/lib/code/highlight.dart new file mode 100644 index 0000000..c66950b --- /dev/null +++ b/example/lib/code/highlight.dart @@ -0,0 +1,106 @@ +/* + * @Description: Code highlight controller + * @Author: ekibun + * @Date: 2020-08-01 17:42:06 + * @LastEditors: ekibun + * @LastEditTime: 2020-08-02 12:39:26 + */ +import 'dart:math'; +import 'dart:ui'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_highlight/themes/a11y-light.dart'; +import 'package:highlight/highlight.dart'; + +Map _theme = a11yLightTheme; +List _convert(String code) { + var nodes = highlight.parse(code, language: 'javascript').nodes; + List spans = []; + var currentSpans = spans; + List> stack = []; + + _traverse(Node node) { + if (node.value != null) { + currentSpans.add(node.className == null + ? TextSpan(text: node.value) + : TextSpan(text: node.value, style: _theme[node.className])); + } else if (node.children != null) { + List tmp = []; + currentSpans.add(TextSpan(children: tmp, style: _theme[node.className])); + stack.add(currentSpans); + currentSpans = tmp; + + node.children.forEach((n) { + _traverse(n); + if (n == node.children.last) { + currentSpans = stack.isEmpty ? spans : stack.removeLast(); + } + }); + } + } + + for (var node in nodes) { + _traverse(node); + } + + return spans; +} + +class CodeInputController extends TextEditingController { + CodeInputController({String text}) : super(text: text); + + TextSpan oldSpan = TextSpan(); + Future spanCall; + + @override + TextSpan buildTextSpan({TextStyle style, bool withComposing}) { + String oldText = oldSpan.toPlainText(); + String newText = value.text; + if (oldText == newText) return oldSpan; + (spanCall?.timeout(Duration.zero) ?? Future.delayed(Duration.zero)) + .then((_) => spanCall = compute(_convert, value.text).then((lsSpan) { + TextSpan newSpan = TextSpan(style: style, children: lsSpan); + if (newSpan.toPlainText() == value.text) oldSpan = newSpan; + notifyListeners(); + })) + .catchError((_) => {}); + + List beforeSpans = []; + int splitAt = value.selection.start; + if (splitAt < 0) splitAt = newText.length ~/ 2; + int start = 0; + InlineSpan leftSpan; + oldSpan.children?.indexWhere((element) { + String elementText = element.toPlainText(); + if (start + elementText.length > splitAt || !newText.startsWith(elementText, start)) { + leftSpan = element; + return true; + } + beforeSpans.add(element); + start += elementText.length; + return false; + }); + List endSpans = []; + int end = 0; + InlineSpan rightSpan; + oldSpan.children?.sublist(beforeSpans.length)?.lastIndexWhere((element) { + String elementText = element.toPlainText(); + if (splitAt + end + elementText.length >= newText.length || + !newText.substring(start, newText.length - end).endsWith(elementText)) { + rightSpan = element; + return true; + } + endSpans.add(element); + end += elementText.length; + return false; + }); + + return TextSpan(style: style, children: [ + ...beforeSpans, + TextSpan( + style: leftSpan != null && leftSpan == rightSpan ? leftSpan.style : style, + text: newText.substring(start, max(start, newText.length - end))), + ...endSpans.reversed + ]); + } +} diff --git a/example/lib/main.dart b/example/lib/main.dart new file mode 100644 index 0000000..fbef0d3 --- /dev/null +++ b/example/lib/main.dart @@ -0,0 +1,39 @@ +/* + * @Description: + * @Author: ekibun + * @Date: 2020-08-08 08:16:51 + * @LastEditors: ekibun + * @LastEditTime: 2020-08-08 17:50:30 + */ +import 'package:flutter/material.dart'; + +import 'package:flutter_qjs_example/test.dart'; + +void main() { + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + + const MyApp({ Key key }) : super(key: key); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'flutter_qjs', + debugShowCheckedModeBanner: false, + theme: ThemeData( + appBarTheme: AppBarTheme(brightness: Brightness.dark, elevation: 0), + primaryColor: Color(0xfff09199), + accentColor: Color(0xffec818a), + backgroundColor: Colors.grey[300], + primaryColorBrightness: Brightness.dark, + ), + routes: { + 'home': (BuildContext context) => TestPage(), + }, + initialRoute: 'home', + ); + } + +} \ No newline at end of file diff --git a/example/lib/test.dart b/example/lib/test.dart new file mode 100644 index 0000000..0694caa --- /dev/null +++ b/example/lib/test.dart @@ -0,0 +1,102 @@ +/* + * @Description: + * @Author: ekibun + * @Date: 2020-07-18 23:28:55 + * @LastEditors: ekibun + * @LastEditTime: 2020-08-08 17:38:48 + */ +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_qjs/flutter_qjs.dart'; + +import 'code/editor.dart'; + +class TestPage extends StatefulWidget { + @override + State createState() => _TestPageState(); +} + +class _TestPageState extends State { + + String code, resp; + int engine; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text("JS 引擎功能测试"), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + FlatButton(child: Text("初始化引擎"), onPressed: () async { + if ((engine?? 0) != 0) return; + engine = await FlutterJs.initEngine(); + // dart 函数回调 + FlutterJs.methodHandler = (String method, List arg) async { + switch (method) { + case "delay": + await Future.delayed(Duration(milliseconds: arg[0])); + return; + case "http": + Response response = await Dio().get(arg[0]); + return response.data; + default: + } + }; + }), + FlatButton(child: Text("运行"), onPressed: () async { + if ((engine?? 0) == 0) { + print("请先初始化引擎"); + return; + } + try { + resp = await FlutterJs.evaluate(code ?? '', ""); + } catch(e) { + resp = e.toString(); + } + setState(() { + code = code; + }); + }), + FlatButton(child: Text("释放引擎"), onPressed: () async { + if ((engine?? 0) == 0) return; + await FlutterJs.close(); + engine = 0; + }), + ], + ), + ), + Container( + padding: const EdgeInsets.all(12), + color: Colors.grey.withOpacity(0.1), + constraints: BoxConstraints(minHeight: 200), + child: CodeEditor( + onChanged: (v) { + code = v; + }, + ), + ), + SizedBox(height: 16), + Text("运行结果:"), + SizedBox(height: 16), + Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + color: Colors.green.withOpacity(0.05), + constraints: BoxConstraints(minHeight: 100), + child: Text(resp ?? ''), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/example/pubspec.lock b/example/pubspec.lock new file mode 100644 index 0000000..e583da5 --- /dev/null +++ b/example/pubspec.lock @@ -0,0 +1,182 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.5.0-nullsafety" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0-nullsafety" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.0-nullsafety.2" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.0-nullsafety" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.0-nullsafety" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.15.0-nullsafety.2" + dio: + dependency: "direct main" + description: + name: dio + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.10" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.0-nullsafety" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_highlight: + dependency: "direct main" + description: + name: flutter_highlight + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.6.0" + flutter_qjs: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "0.0.1" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + highlight: + dependency: "direct main" + description: + name: highlight + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.6.0" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.4" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.12.10-nullsafety" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.0-nullsafety.2" + path: + dependency: transitive + description: + name: path + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.8.0-nullsafety" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.8.0-nullsafety" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.10.0-nullsafety" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0-nullsafety" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.0-nullsafety" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.0-nullsafety" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.19-nullsafety" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.0-nullsafety.2" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0-nullsafety.2" +sdks: + dart: ">=2.10.0-0.0.dev <2.10.0" + flutter: ">=1.20.0 <2.0.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml new file mode 100644 index 0000000..a248b35 --- /dev/null +++ b/example/pubspec.yaml @@ -0,0 +1,71 @@ +name: flutter_qjs_example +description: Demonstrates how to use the flutter_qjs plugin. + +# The following line prevents the package from being accidentally published to +# pub.dev using `pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +environment: + sdk: ">=2.7.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + + flutter_qjs: + # When depending on this package from a real application you should use: + # flutter_qjs: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + + highlight: 0.6.0 + flutter_highlight: 0.6.0 + dio: 3.0.10 + +dev_dependencies: + flutter_test: + sdk: flutter + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart new file mode 100644 index 0000000..a335416 --- /dev/null +++ b/example/test/widget_test.dart @@ -0,0 +1,27 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility that Flutter provides. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:flutter_qjs_example/main.dart'; + +void main() { + testWidgets('Verify Platform version', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(MyApp()); + + // Verify that platform version is retrieved. + expect( + find.byWidgetPredicate( + (Widget widget) => widget is Text && + widget.data.startsWith('Running on:'), + ), + findsOneWidget, + ); + }); +} diff --git a/example/windows/.gitignore b/example/windows/.gitignore new file mode 100644 index 0000000..d492d0d --- /dev/null +++ b/example/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/example/windows/CMakeLists.txt b/example/windows/CMakeLists.txt new file mode 100644 index 0000000..ea7bfdf --- /dev/null +++ b/example/windows/CMakeLists.txt @@ -0,0 +1,95 @@ +cmake_minimum_required(VERSION 3.15) +project(flutter_qjs_example LANGUAGES CXX) + +set(BINARY_NAME "flutter_qjs_example") + +cmake_policy(SET CMP0063 NEW) + +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Configure build options. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() + +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX- /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") + +# Flutter library and tool build rules. +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build +add_subdirectory("runner") + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/example/windows/flutter/.template_version b/example/windows/flutter/.template_version new file mode 100644 index 0000000..b8626c4 --- /dev/null +++ b/example/windows/flutter/.template_version @@ -0,0 +1 @@ +4 diff --git a/example/windows/flutter/CMakeLists.txt b/example/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..3c1726f --- /dev/null +++ b/example/windows/flutter/CMakeLists.txt @@ -0,0 +1,98 @@ +cmake_minimum_required(VERSION 3.15) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "engine_method_result.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + windows-x64 $ +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/example/windows/flutter/generated_plugin_registrant.cc b/example/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..5893996 --- /dev/null +++ b/example/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,12 @@ +// +// Generated file. Do not edit. +// + +#include "generated_plugin_registrant.h" + +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + FlutterQjsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterQjsPlugin")); +} diff --git a/example/windows/flutter/generated_plugin_registrant.h b/example/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..9846246 --- /dev/null +++ b/example/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,13 @@ +// +// Generated file. Do not edit. +// + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/example/windows/flutter/generated_plugins.cmake b/example/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..59556b2 --- /dev/null +++ b/example/windows/flutter/generated_plugins.cmake @@ -0,0 +1,16 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + flutter_qjs +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) diff --git a/example/windows/runner/CMakeLists.txt b/example/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..83e5aca --- /dev/null +++ b/example/windows/runner/CMakeLists.txt @@ -0,0 +1,18 @@ +cmake_minimum_required(VERSION 3.15) +project(runner LANGUAGES CXX) + +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "run_loop.cpp" + "utils.cpp" + "win32_window.cpp" + "window_configuration.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) +apply_standard_settings(${BINARY_NAME}) +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/example/windows/runner/Runner.rc b/example/windows/runner/Runner.rc new file mode 100644 index 0000000..5b41a82 --- /dev/null +++ b/example/windows/runner/Runner.rc @@ -0,0 +1,70 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/example/windows/runner/flutter_window.cpp b/example/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..fe980cf --- /dev/null +++ b/example/windows/runner/flutter_window.cpp @@ -0,0 +1,29 @@ +#include "flutter_window.h" + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(RunLoop* run_loop, + const flutter::DartProject& project) + : run_loop_(run_loop), project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +void FlutterWindow::OnCreate() { + Win32Window::OnCreate(); + + // The size here is arbitrary since SetChildContent will resize it. + flutter_controller_ = + std::make_unique(100, 100, project_); + RegisterPlugins(flutter_controller_.get()); + run_loop_->RegisterFlutterInstance(flutter_controller_.get()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + run_loop_->UnregisterFlutterInstance(flutter_controller_.get()); + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} diff --git a/example/windows/runner/flutter_window.h b/example/windows/runner/flutter_window.h new file mode 100644 index 0000000..4f41e16 --- /dev/null +++ b/example/windows/runner/flutter_window.h @@ -0,0 +1,37 @@ +#ifndef FLUTTER_WINDOW_H_ +#define FLUTTER_WINDOW_H_ + +#include +#include + +#include "run_loop.h" +#include "win32_window.h" + +#include + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow driven by the |run_loop|, hosting a + // Flutter view running |project|. + explicit FlutterWindow(RunLoop* run_loop, + const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + void OnCreate() override; + void OnDestroy() override; + + private: + // The run loop driving events for this window. + RunLoop* run_loop_; + + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // FLUTTER_WINDOW_H_ diff --git a/example/windows/runner/main.cpp b/example/windows/runner/main.cpp new file mode 100644 index 0000000..11b48e9 --- /dev/null +++ b/example/windows/runner/main.cpp @@ -0,0 +1,37 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "run_loop.h" +#include "utils.h" +#include "window_configuration.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + RunLoop run_loop; + + flutter::DartProject project(L"data"); + FlutterWindow window(&run_loop, project); + Win32Window::Point origin(kFlutterWindowOriginX, kFlutterWindowOriginY); + Win32Window::Size size(kFlutterWindowWidth, kFlutterWindowHeight); + if (!window.CreateAndShow(kFlutterWindowTitle, origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + run_loop.Run(); + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/example/windows/runner/resource.h b/example/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/example/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/example/windows/runner/resources/app_icon.ico b/example/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000..c04e20c Binary files /dev/null and b/example/windows/runner/resources/app_icon.ico differ diff --git a/example/windows/runner/run_loop.cpp b/example/windows/runner/run_loop.cpp new file mode 100644 index 0000000..f91d6d4 --- /dev/null +++ b/example/windows/runner/run_loop.cpp @@ -0,0 +1,70 @@ +#include "run_loop.h" + +#include +// Don't stomp std::min/std::max +#undef max +#undef min + +#include + +RunLoop::RunLoop() {} + +RunLoop::~RunLoop() {} + +void RunLoop::Run() { + bool keep_running = true; + TimePoint next_flutter_event_time = TimePoint::clock::now(); + while (keep_running) { + std::chrono::nanoseconds wait_duration = + std::max(std::chrono::nanoseconds(0), + next_flutter_event_time - TimePoint::clock::now()); + ::MsgWaitForMultipleObjects( + 0, nullptr, FALSE, static_cast(wait_duration.count() / 1000), + QS_ALLINPUT); + bool processed_events = false; + MSG message; + // All pending Windows messages must be processed; MsgWaitForMultipleObjects + // won't return again for items left in the queue after PeekMessage. + while (::PeekMessage(&message, nullptr, 0, 0, PM_REMOVE)) { + processed_events = true; + if (message.message == WM_QUIT) { + keep_running = false; + break; + } + ::TranslateMessage(&message); + ::DispatchMessage(&message); + // Allow Flutter to process messages each time a Windows message is + // processed, to prevent starvation. + next_flutter_event_time = + std::min(next_flutter_event_time, ProcessFlutterMessages()); + } + // If the PeekMessage loop didn't run, process Flutter messages. + if (!processed_events) { + next_flutter_event_time = + std::min(next_flutter_event_time, ProcessFlutterMessages()); + } + } +} + +void RunLoop::RegisterFlutterInstance( + flutter::FlutterViewController* flutter_instance) { + flutter_instances_.insert(flutter_instance); +} + +void RunLoop::UnregisterFlutterInstance( + flutter::FlutterViewController* flutter_instance) { + flutter_instances_.erase(flutter_instance); +} + +RunLoop::TimePoint RunLoop::ProcessFlutterMessages() { + TimePoint next_event_time = TimePoint::max(); + for (auto flutter_controller : flutter_instances_) { + std::chrono::nanoseconds wait_duration = + flutter_controller->ProcessMessages(); + if (wait_duration != std::chrono::nanoseconds::max()) { + next_event_time = + std::min(next_event_time, TimePoint::clock::now() + wait_duration); + } + } + return next_event_time; +} diff --git a/example/windows/runner/run_loop.h b/example/windows/runner/run_loop.h new file mode 100644 index 0000000..442a58e --- /dev/null +++ b/example/windows/runner/run_loop.h @@ -0,0 +1,40 @@ +#ifndef RUN_LOOP_H_ +#define RUN_LOOP_H_ + +#include + +#include +#include + +// A runloop that will service events for Flutter instances as well +// as native messages. +class RunLoop { + public: + RunLoop(); + ~RunLoop(); + + // Prevent copying + RunLoop(RunLoop const&) = delete; + RunLoop& operator=(RunLoop const&) = delete; + + // Runs the run loop until the application quits. + void Run(); + + // Registers the given Flutter instance for event servicing. + void RegisterFlutterInstance( + flutter::FlutterViewController* flutter_instance); + + // Unregisters the given Flutter instance from event servicing. + void UnregisterFlutterInstance( + flutter::FlutterViewController* flutter_instance); + + private: + using TimePoint = std::chrono::steady_clock::time_point; + + // Processes all currently pending messages for registered Flutter instances. + TimePoint ProcessFlutterMessages(); + + std::set flutter_instances_; +}; + +#endif // RUN_LOOP_H_ diff --git a/example/windows/runner/runner.exe.manifest b/example/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..c977c4a --- /dev/null +++ b/example/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/example/windows/runner/utils.cpp b/example/windows/runner/utils.cpp new file mode 100644 index 0000000..37501e5 --- /dev/null +++ b/example/windows/runner/utils.cpp @@ -0,0 +1,22 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} diff --git a/example/windows/runner/utils.h b/example/windows/runner/utils.h new file mode 100644 index 0000000..d247a66 --- /dev/null +++ b/example/windows/runner/utils.h @@ -0,0 +1,8 @@ +#ifndef CONSOLE_UTILS_H_ +#define CONSOLE_UTILS_H_ + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +#endif // CONSOLE_UTILS_H_ diff --git a/example/windows/runner/win32_window.cpp b/example/windows/runner/win32_window.cpp new file mode 100644 index 0000000..677a9a6 --- /dev/null +++ b/example/windows/runner/win32_window.cpp @@ -0,0 +1,249 @@ +#include "win32_window.h" + +#include + +#include "resource.h" + +namespace { + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + FreeLibrary(user32_module); + } +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + OnCreate(); + + return window != nullptr; +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + auto window = + reinterpret_cast(GetWindowLongPtr(hwnd, GWLP_USERDATA)); + + if (window == nullptr) { + return 0; + } + + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: + RECT rect; + GetClientRect(hwnd, &rect); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + 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); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame; + GetClientRect(window_handle_, &frame); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +void Win32Window::OnCreate() { + // No-op; provided for subclasses. +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} diff --git a/example/windows/runner/win32_window.h b/example/windows/runner/win32_window.h new file mode 100644 index 0000000..5cbb5d5 --- /dev/null +++ b/example/windows/runner/win32_window.h @@ -0,0 +1,96 @@ +#ifndef WIN32_WINDOW_H_ +#define WIN32_WINDOW_H_ + +#include +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates and shows a win32 window with |title| and position and size using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size to will treat the width height passed in to this function + // as logical pixels and scale to appropriate for the default monitor. Returns + // true if the window was created successfully. + bool CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. + virtual void OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responsponds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // WIN32_WINDOW_H_ diff --git a/example/windows/runner/window_configuration.cpp b/example/windows/runner/window_configuration.cpp new file mode 100644 index 0000000..5d27ba9 --- /dev/null +++ b/example/windows/runner/window_configuration.cpp @@ -0,0 +1,7 @@ +#include "window_configuration.h" + +const wchar_t* kFlutterWindowTitle = L"flutter_qjs_example"; +const unsigned int kFlutterWindowOriginX = 10; +const unsigned int kFlutterWindowOriginY = 10; +const unsigned int kFlutterWindowWidth = 1280; +const unsigned int kFlutterWindowHeight = 720; diff --git a/example/windows/runner/window_configuration.h b/example/windows/runner/window_configuration.h new file mode 100644 index 0000000..ea5cead --- /dev/null +++ b/example/windows/runner/window_configuration.h @@ -0,0 +1,18 @@ +#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_ diff --git a/flutter_qjs.iml b/flutter_qjs.iml new file mode 100644 index 0000000..429df7d --- /dev/null +++ b/flutter_qjs.iml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lib/flutter_qjs.dart b/lib/flutter_qjs.dart new file mode 100644 index 0000000..93b182a --- /dev/null +++ b/lib/flutter_qjs.dart @@ -0,0 +1,37 @@ +/* + * @Description: + * @Author: ekibun + * @Date: 2020-08-08 08:29:09 + * @LastEditors: ekibun + * @LastEditTime: 2020-08-08 17:40:35 + */ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/services.dart'; + +class FlutterJs { + static const MethodChannel _channel = const MethodChannel('soko.ekibun.flutter_qjs'); + + static Future Function(String method, List args) methodHandler; + + static Future initEngine() async { + final int engineId = await _channel.invokeMethod("initEngine"); + _channel.setMethodCallHandler((call) async { + if (methodHandler == null) return call.noSuchMethod(null); + List args = jsonDecode(call.arguments); + return jsonEncode(await methodHandler(call.method, args)); + }); + return engineId; + } + + static Future evaluate(String command, String name) async { + var arguments = {"script": command, "name": command}; + final String jsResult = await _channel.invokeMethod("evaluate", arguments); + return jsResult ?? "null"; + } + + static Future close() async { + return await _channel.invokeMethod("close"); + } +} diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..6111eff --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,147 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.5.0-nullsafety" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0-nullsafety" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.0-nullsafety.2" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.0-nullsafety" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.0-nullsafety" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.15.0-nullsafety.2" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.0-nullsafety" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.12.10-nullsafety" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.0-nullsafety.2" + path: + dependency: transitive + description: + name: path + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.8.0-nullsafety" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.8.0-nullsafety" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.10.0-nullsafety" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0-nullsafety" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.0-nullsafety" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.0-nullsafety" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.19-nullsafety" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.0-nullsafety.2" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0-nullsafety.2" +sdks: + dart: ">=2.10.0-0.0.dev <2.10.0" + flutter: ">=1.20.0 <2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..03e9cea --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,68 @@ +name: flutter_qjs +description: A new flutter plugin project. +version: 0.0.1 +author: +homepage: + +environment: + sdk: ">=2.7.0 <3.0.0" + flutter: ">=1.20.0 <2.0.0" + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + # This section identifies this Flutter project as a plugin project. + # The 'pluginClass' and Android 'package' identifiers should not ordinarily + # be modified. They are used by the tooling to maintain consistency when + # adding or updating assets for this project. + plugin: + platforms: + # This plugin project was generated without specifying any + # platforms with the `--platform` argument. If you see the `fake_platform` map below, remove it and + # then add platforms following the instruction here: + # https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin-platforms + # ------------------- + windows: + pluginClass: FlutterQjsPlugin + # ------------------- + + # To add assets to your plugin package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # To add custom fonts to your plugin package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/test/flutter_qjs_test.dart b/test/flutter_qjs_test.dart new file mode 100644 index 0000000..17a1f88 --- /dev/null +++ b/test/flutter_qjs_test.dart @@ -0,0 +1,19 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + const MethodChannel channel = MethodChannel('flutter_qjs'); + + TestWidgetsFlutterBinding.ensureInitialized(); + + setUp(() { + channel.setMockMethodCallHandler((MethodCall methodCall) async { + return '42'; + }); + }); + + tearDown(() { + channel.setMockMethodCallHandler(null); + }); + +} diff --git a/windows/.gitignore b/windows/.gitignore new file mode 100644 index 0000000..b3eb2be --- /dev/null +++ b/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt new file mode 100644 index 0000000..873038e --- /dev/null +++ b/windows/CMakeLists.txt @@ -0,0 +1,25 @@ +cmake_minimum_required(VERSION 3.15) +set(PROJECT_NAME "flutter_qjs") +project(${PROJECT_NAME} LANGUAGES CXX) +add_compile_options("$<$:/utf-8>") + +set(PLUGIN_NAME "${PROJECT_NAME}_plugin") + +add_library(${PLUGIN_NAME} SHARED + "${PLUGIN_NAME}.cpp" +) +apply_standard_settings(${PLUGIN_NAME}) +set_target_properties(${PLUGIN_NAME} PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL) +target_include_directories(${PLUGIN_NAME} INTERFACE + "${CMAKE_CURRENT_SOURCE_DIR}/include") +target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin + "${CMAKE_CURRENT_SOURCE_DIR}/quickjs/libquickjs64.a" +) + +# List of absolute paths to libraries that should be bundled with the plugin +set(flutter_qjs_bundled_libraries + "${CMAKE_CURRENT_SOURCE_DIR}/quickjs/libquickjs64.dll" + PARENT_SCOPE +) diff --git a/windows/flutter_qjs_plugin.cpp b/windows/flutter_qjs_plugin.cpp new file mode 100644 index 0000000..b5ef743 --- /dev/null +++ b/windows/flutter_qjs_plugin.cpp @@ -0,0 +1,123 @@ +#include "include/flutter_qjs/flutter_qjs_plugin.h" + +// This must be included before many other Windows headers. +#include + +// For getPlatformVersion; remove unless needed for your plugin implementation. +#include + +#include +#include +#include + +#include "js_engine.hpp" + +namespace +{ + + class FlutterQjsPlugin : public flutter::Plugin + { + public: + static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar); + + FlutterQjsPlugin(); + + virtual ~FlutterQjsPlugin(); + + private: + // Called when a method is called on this plugin's channel from Dart. + void HandleMethodCall( + const flutter::MethodCall &method_call, + std::unique_ptr> result); + }; + + std::shared_ptr> channel; + + // static + void FlutterQjsPlugin::RegisterWithRegistrar( + flutter::PluginRegistrarWindows *registrar) + { + channel = + std::make_unique>( + registrar->messenger(), "soko.ekibun.flutter_qjs", + &flutter::StandardMethodCodec::GetInstance()); + + auto plugin = std::make_unique(); + + 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() {} + + 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; + } + + qjs::Engine *engine = nullptr; + + void FlutterQjsPlugin::HandleMethodCall( + const flutter::MethodCall &method_call, + std::unique_ptr> 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("initEngine") == 0) + { + engine = new qjs::Engine(channel); + flutter::EncodableValue response((long)engine); + result->Success(&response); + } + else if (method_call.method_name().compare("evaluate") == 0) + { + flutter::EncodableMap args = *((flutter::EncodableMap *)method_call.arguments()); + std::string script = std::get(ValueOrNull(args, "script")); + std::string name = std::get(ValueOrNull(args, "name")); + auto presult = result.release(); + engine->commit(qjs::EngineTask{ + script, name, + [presult](std::string resolve) { + flutter::EncodableValue response(resolve); + presult->Success(&response); + }, + [presult](std::string reject) { + presult->Error("FlutterJSException", reject); + }}); + } + else if (method_call.method_name().compare("close") == 0) + { + delete engine; + result->Success(); + } + else + { + result->NotImplemented(); + } + } + +} // namespace + +void FlutterQjsPluginRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar) +{ + FlutterQjsPlugin::RegisterWithRegistrar( + flutter::PluginRegistrarManager::GetInstance() + ->GetRegistrar(registrar)); +} diff --git a/windows/include/flutter_qjs/flutter_qjs_plugin.h b/windows/include/flutter_qjs/flutter_qjs_plugin.h new file mode 100644 index 0000000..cc4130d --- /dev/null +++ b/windows/include/flutter_qjs/flutter_qjs_plugin.h @@ -0,0 +1,23 @@ +#ifndef FLUTTER_PLUGIN_FLUTTER_JS_PLUGIN_H_ +#define FLUTTER_PLUGIN_FLUTTER_JS_PLUGIN_H_ + +#include + +#ifdef FLUTTER_PLUGIN_IMPL +#define FLUTTER_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FLUTTER_PLUGIN_EXPORT __declspec(dllimport) +#endif + +#if defined(__cplusplus) +extern "C" { +#endif + +FLUTTER_PLUGIN_EXPORT void FlutterQjsPluginRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar); + +#if defined(__cplusplus) +} // extern "C" +#endif + +#endif // FLUTTER_PLUGIN_FLUTTER_JS_PLUGIN_H_ diff --git a/windows/js_dart_promise.hpp b/windows/js_dart_promise.hpp new file mode 100644 index 0000000..ff2e899 --- /dev/null +++ b/windows/js_dart_promise.hpp @@ -0,0 +1,207 @@ +/* + * @Description: + * @Author: ekibun + * @Date: 2020-08-07 13:55:52 + * @LastEditors: ekibun + * @LastEditTime: 2020-08-08 16:54:23 + */ +#pragma once +#include "quickjs/quickjspp.hpp" +#include "quickjs/quickjs/list.h" +#include +#include + +namespace qjs +{ + static JSClassID js_dart_promise_class_id; + + typedef struct + { + int count; + JSValue *argv; + } JSOSFutureArgv; + + using JSFutureReturn = std::function; + + typedef struct + { + struct list_head link; + std::shared_future future; + JSValue resolve; + JSValue reject; + } JSOSFuture; + + typedef struct JSThreadState + { + struct list_head os_future; /* list of JSOSFuture.link */ + std::shared_ptr> channel; + } JSThreadState; + + static JSValue js_add_future(Value resolve, Value reject, std::shared_future future) + { + JSRuntime *rt = JS_GetRuntime(resolve.ctx); + JSThreadState *ts = (JSThreadState *)JS_GetRuntimeOpaque(rt); + JSValueConst jsResolve, jsReject; + JSOSFuture *th; + JSValue obj; + + jsResolve = resolve.v; + if (!JS_IsFunction(resolve.ctx, jsResolve)) + return JS_ThrowTypeError(resolve.ctx, "resolve not a function"); + jsReject = reject.v; + if (!JS_IsFunction(reject.ctx, jsReject)) + return JS_ThrowTypeError(reject.ctx, "reject not a function"); + obj = JS_NewObjectClass(resolve.ctx, js_dart_promise_class_id); + if (JS_IsException(obj)) + return obj; + th = (JSOSFuture *)js_mallocz(resolve.ctx, sizeof(*th)); + if (!th) + { + JS_FreeValue(resolve.ctx, obj); + return JS_EXCEPTION; + } + th->future = future; + th->resolve = JS_DupValue(resolve.ctx, jsResolve); + th->reject = JS_DupValue(reject.ctx, jsReject); + list_add_tail(&th->link, &ts->os_future); + JS_SetOpaque(obj, th); + return obj; + } + + JSValue js_dart_future(Value resolve, Value reject, std::string name, std::string args) + { + auto promise = new std::promise(); + JSRuntime *rt = JS_GetRuntime(resolve.ctx); + JSThreadState *ts = (JSThreadState *)JS_GetRuntimeOpaque(rt); + ts->channel->InvokeMethod( + name, + std::make_unique(args), + std::make_unique>( + (flutter::ResultHandlerSuccess)[promise]( + const flutter::EncodableValue *result) { + promise->set_value((JSFutureReturn)[rep = std::get(*result)](JSContext * ctx) { + JSValue *ret = new JSValue{JS_NewString(ctx, rep.c_str())}; + return JSOSFutureArgv{1, ret}; + }); + }, + (flutter::ResultHandlerError)[promise]( + const std::string &error_code, + const std::string &error_message, + const flutter::EncodableValue *error_details) { + promise->set_value((JSFutureReturn)[error_message](JSContext * ctx) { + JSValue *ret = new JSValue{JS_NewString(ctx, error_message.c_str())}; + return JSOSFutureArgv{-1, ret}; + }); + }, + (flutter::ResultHandlerNotImplemented)[promise]() { + promise->set_value((JSFutureReturn)[](JSContext * ctx) { + JSValue *ret = new JSValue{JS_NewString(ctx, "NotImplemented")}; + return JSOSFutureArgv{-1, ret}; + }); + })); + return js_add_future(resolve, reject, promise->get_future()); + } + + static void unlink_future(JSRuntime *rt, JSOSFuture *th) + { + if (th->link.prev) + { + list_del(&th->link); + th->link.prev = th->link.next = NULL; + } + } + + static void free_future(JSRuntime *rt, JSOSFuture *th) + { + JS_FreeValueRT(rt, th->resolve); + JS_FreeValueRT(rt, th->reject); + js_free_rt(rt, th); + } + + void js_init_handlers(JSRuntime *rt, std::shared_ptr> channel) + { + JSThreadState *ts = (JSThreadState *)malloc(sizeof(*ts)); + if (!ts) + { + fprintf(stderr, "Could not allocate memory for the worker"); + exit(1); + } + memset(ts, 0, sizeof(*ts)); + init_list_head(&ts->os_future); + ts->channel = channel; + + JS_SetRuntimeOpaque(rt, ts); + } + + void js_free_handlers(JSRuntime *rt) + { + JSThreadState *ts = (JSThreadState *)JS_GetRuntimeOpaque(rt); + struct list_head *el, *el1; + + list_for_each_safe(el, el1, &ts->os_future) + { + JSOSFuture *th = list_entry(el, JSOSFuture, link); + th->future.get(); + unlink_future(rt, th); + free_future(rt, th); + } + ts->channel = nullptr; + free(ts); + JS_SetRuntimeOpaque(rt, NULL); /* fail safe */ + } + + static void call_handler(JSContext *ctx, JSValueConst func, int count, JSValue *argv) + { + JSValue ret, func1; + /* 'func' might be destroyed when calling itself (if it frees the + handler), so must take extra care */ + func1 = JS_DupValue(ctx, func); + ret = JS_Call(ctx, func1, JS_UNDEFINED, count, argv); + JS_FreeValue(ctx, func1); + if (JS_IsException(ret)) + throw exception{}; + JS_FreeValue(ctx, ret); + } + + static int js_dart_poll(JSContext *ctx) + { + JSRuntime *rt = JS_GetRuntime(ctx); + JSThreadState *ts = (JSThreadState *)JS_GetRuntimeOpaque(rt); + struct list_head *el; + + /* XXX: handle signals if useful */ + + if (list_empty(&ts->os_future)) + return -1; /* no more events */ + + /* XXX: only timers and basic console input are supported */ + if (!list_empty(&ts->os_future)) + { + list_for_each(el, &ts->os_future) + { + JSOSFuture *th = list_entry(el, JSOSFuture, link); + if (th->future._Is_ready()) + { + JSOSFutureArgv argv = th->future.get()(ctx); + JSValue resolve, reject; + int64_t delay; + /* the timer expired */ + resolve = th->resolve; + th->resolve = JS_UNDEFINED; + reject = th->reject; + th->reject = JS_UNDEFINED; + unlink_future(rt, th); + free_future(rt, th); + call_handler(ctx, argv.count < 0 ? reject : resolve, abs(argv.count), argv.argv); + for (int i = 0; i < abs(argv.count); ++i) + JS_FreeValue(ctx, argv.argv[i]); + JS_FreeValue(ctx, resolve); + JS_FreeValue(ctx, reject); + delete argv.argv; + return 0; + } + } + } + return 0; + } +} // namespace qjs \ No newline at end of file diff --git a/windows/js_engine.hpp b/windows/js_engine.hpp new file mode 100644 index 0000000..dfe8e30 --- /dev/null +++ b/windows/js_engine.hpp @@ -0,0 +1,192 @@ +/* + * @Description: + * @Author: ekibun + * @Date: 2020-08-08 10:30:59 + * @LastEditors: ekibun + * @LastEditTime: 2020-08-08 17:05:22 + */ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "js_dart_promise.hpp" +#include "quickjs/quickjspp.hpp" + +namespace qjs +{ + struct EngineTask + { + std::string command; + std::string name; + std::function resolve; + std::function reject; + }; + + struct EngineTaskResolver + { + Value result; + std::function resolve; + std::function reject; + }; + + std::string getStackTrack(Value exc) + { + std::string err = (std::string)exc; + if ((bool)exc["stack"]) + err += "\n" + (std::string)exc["stack"]; + return err; + } + + class Engine + { + // 引擎线程 + std::thread thread; + // 任务队列 + std::queue tasks; + // 同步 + std::mutex m_lock; + // 是否关闭提交 + std::atomic stoped; + + void handleException(qjs::Value exc) + { + std::cout << getStackTrack(exc) << std::endl; + } + + public: + inline Engine(std::shared_ptr> channel) : stoped{false} + { + thread = std::thread([this, channel] { // 工作线程函数 + // 创建运行环境 + Runtime rt; + js_init_handlers(rt.rt, channel); + Context ctx(rt); + auto &module = ctx.addModule("__DartImpl"); + module.function<&js_dart_future>("__invoke"); + ctx.eval( + R"xxx( + import * as __DartImpl from "__DartImpl"; + globalThis.dart = (method, ...args) => new Promise((res, rej) => + __DartImpl.__invoke((v) => res(JSON.parse(v)), rej, method, JSON.stringify(args))); + )xxx", + "", JS_EVAL_TYPE_MODULE); + std::vector unresolvedTask; + // 循环 + while (!this->stoped) + { + // 获取待执行的task + EngineTask task; + { // 获取一个待执行的 task + std::unique_lock lock{this->m_lock}; // unique_lock 相比 lock_guard 的好处是:可以随时 unlock() 和 lock() + if (!this->tasks.empty()) + { + task = this->tasks.front(); // 取一个 task + this->tasks.pop(); + } + } + // 执行task + if (task.resolve) + try + { + ctx.global()["__evalstr"] = JS_NewString(ctx.ctx, task.command.c_str()); + Value ret = ctx.eval( + R"xxx( + (() => { + const __ret = Promise.resolve(eval(__evalstr)) + .then(v => { + __ret.__value = v; + __ret.__resolved = true; + }).catch(e => { + __ret.__error = e; + __ret.__rejected = true; + }); + return __ret; + })() + )xxx", + task.name.c_str()); + unresolvedTask.emplace_back(EngineTaskResolver{ret, std::move(task.resolve), std::move(task.reject)}); + } + catch (exception e) + { + task.reject(getStackTrack(ctx.getException())); + } + // 执行microtask + JSContext *pctx; + for (;;) + { + int err = JS_ExecutePendingJob(rt.rt, &pctx); + if (err <= 0) + { + if (err < 0) + std::cout << getStackTrack(ctx.getException()) << std::endl; + break; + } + } + // TODO 检查promise状态 + for (auto it = unresolvedTask.begin(); it != unresolvedTask.end();) + { + bool finished = false; + if (it->result["__resolved"]) + { + it->resolve((std::string)it->result["__value"]); + finished = true; + }; + if (it->result["__rejected"]) + { + it->reject(getStackTrack(it->result["__error"])); + finished = true; + }; + if (finished) + it = unresolvedTask.erase(it); + else + ++it; + } + // 检查dart交互 + bool idle = true; + try + { + idle = js_dart_poll(ctx.ctx); + } + catch (exception e) + { + handleException(ctx.getException()); + } + // 空闲时reject所有task + if (idle && !JS_IsJobPending(rt.rt) && !unresolvedTask.empty()) + { + for (EngineTaskResolver &task : unresolvedTask) + { + task.reject("Promise cannot resolve"); + } + unresolvedTask.clear(); + } + } + js_free_handlers(rt.rt); + }); + } + inline ~Engine() + { + stoped.store(true); + if (thread.joinable()) + thread.join(); // 等待任务结束, 前提:线程一定会执行完 + } + + public: + // 提交一个任务 + void commit(EngineTask task) + { + if (stoped.load()) // stop == true ?? + throw std::runtime_error("commit on stopped engine."); + { // 添加任务到队列 + std::lock_guard lock{m_lock}; //对当前块的语句加锁 lock_guard 是 mutex 的 stack 封装类,构造的时候 lock(),析构的时候 unlock() + tasks.emplace(task); + } + } + }; + +} // namespace qjs diff --git a/windows/quickjs/libquickjs.def b/windows/quickjs/libquickjs.def new file mode 100644 index 0000000..b99cd50 --- /dev/null +++ b/windows/quickjs/libquickjs.def @@ -0,0 +1,195 @@ +EXPORTS +JS_AddIntrinsicAtomics +JS_AddIntrinsicBaseObjects +JS_AddIntrinsicBigDecimal +JS_AddIntrinsicBigFloat +JS_AddIntrinsicBigInt +JS_AddIntrinsicDate +JS_AddIntrinsicEval +JS_AddIntrinsicJSON +JS_AddIntrinsicMapSet +JS_AddIntrinsicOperators +JS_AddIntrinsicPromise +JS_AddIntrinsicProxy +JS_AddIntrinsicRegExp +JS_AddIntrinsicRegExpCompiler +JS_AddIntrinsicStringNormalize +JS_AddIntrinsicTypedArrays +JS_AddModuleExport +JS_AddModuleExportList +JS_AtomToCString +JS_AtomToString +JS_AtomToValue +JS_Call +JS_CallConstructor +JS_CallConstructor2 +JS_ComputeMemoryUsage +JS_DefineProperty +JS_DefinePropertyGetSet +JS_DefinePropertyValue +JS_DefinePropertyValueInt64 +JS_DefinePropertyValueStr +JS_DefinePropertyValueUint32 +JS_DefinePropertyValueValue +JS_DeleteProperty +JS_DeletePropertyInt64 +JS_DetachArrayBuffer +JS_DetectModule +JS_DumpMemoryUsage +JS_DupAtom +JS_DupContext +JS_EnableBignumExt +JS_EnqueueJob +JS_Eval +JS_EvalFunction +JS_ExecutePendingJob +JS_FreeAtom +JS_FreeAtomRT +JS_FreeCString +JS_FreeContext +JS_FreeRuntime +JS_GetArrayBuffer +JS_GetClassProto +JS_GetContextOpaque +JS_GetException +JS_GetGlobalObject +JS_GetImportMeta +JS_GetModuleName +JS_GetOpaque +JS_GetOpaque2 +JS_GetOwnProperty +JS_GetOwnPropertyNames +JS_GetPropertyInternal +JS_GetPropertyStr +JS_GetPropertyUint32 +JS_GetPrototype +JS_GetRuntime +JS_GetRuntimeOpaque +JS_GetTypedArrayBuffer +JS_HasProperty +JS_Invoke +JS_IsArray +JS_IsCFunction +JS_IsConstructor +JS_IsError +JS_IsExtensible +JS_IsFunction +JS_IsInstanceOf +JS_IsJobPending +JS_IsLiveObject +JS_IsRegisteredClass +JS_IsUncatchableError +JS_JSONStringify +JS_MarkValue +JS_NewArray +JS_NewArrayBuffer +JS_NewArrayBufferCopy +JS_NewAtom +JS_NewAtomLen +JS_NewAtomString +JS_NewAtomUInt32 +JS_NewBigInt64 +JS_NewBigInt64_1 +JS_NewBigUint64 +JS_NewCFunction2 +JS_NewCFunctionData +JS_NewCModule +JS_NewClass +JS_NewClassID +JS_NewContext +JS_NewContextRaw +JS_NewError +JS_NewObject +JS_NewObjectClass +JS_NewObjectProto +JS_NewObjectProtoClass +JS_NewPromiseCapability +JS_NewRuntime +JS_NewRuntime2 +JS_NewString +JS_NewStringLen +JS_ParseJSON +JS_ParseJSON2 +JS_PreventExtensions +JS_ReadObject +JS_ResetUncatchableError +JS_ResolveModule +JS_RunGC +JS_SetCanBlock +JS_SetClassProto +JS_SetConstructor +JS_SetConstructorBit +JS_SetContextOpaque +JS_SetGCThreshold +JS_SetHostPromiseRejectionTracker +JS_SetInterruptHandler +JS_SetMaxStackSize +JS_SetMemoryLimit +JS_SetModuleExport +JS_SetModuleExportList +JS_SetModuleLoaderFunc +JS_SetOpaque +JS_SetPropertyFunctionList +JS_SetPropertyInt64 +JS_SetPropertyInternal +JS_SetPropertyStr +JS_SetPropertyUint32 +JS_SetPrototype +JS_SetRuntimeInfo +JS_SetRuntimeOpaque +JS_SetSharedArrayBufferFunctions +JS_SetUncatchableError +JS_Throw +JS_ThrowInternalError +JS_ThrowOutOfMemory +JS_ThrowRangeError +JS_ThrowReferenceError +JS_ThrowSyntaxError +JS_ThrowTypeError +JS_ToBigInt64 +JS_ToBool +JS_ToCStringLen2 +JS_ToFloat64 +JS_ToIndex +JS_ToInt32 +JS_ToInt32Clamp +JS_ToInt32Sat +JS_ToInt64 +JS_ToInt64Clamp +JS_ToInt64Ext +JS_ToInt64Sat +JS_ToPropertyKey +JS_ToString +JS_ToStringInternal +JS_ValueToAtom +JS_WriteObject +JS_WriteObject2 +__JS_FreeValue +__JS_FreeValueRT +js_free +js_free_rt +js_init_module_os +js_init_module_std +js_load_file +js_malloc +js_malloc_rt +js_malloc_usable_size +js_malloc_usable_size_rt +js_mallocz +js_mallocz_rt +js_module_loader +js_module_set_import_meta +js_parse_error +js_realloc +js_realloc2 +js_realloc_rt +js_std_add_helpers +js_std_dump_error +js_std_eval_binary +js_std_free_handlers +js_std_init_handlers +js_std_loop +js_std_promise_rejection_tracker +js_strdup +js_string_codePointRange +js_strndup diff --git a/windows/quickjs/libquickjs32.a b/windows/quickjs/libquickjs32.a new file mode 100644 index 0000000..32191d8 Binary files /dev/null and b/windows/quickjs/libquickjs32.a differ diff --git a/windows/quickjs/libquickjs32.dll b/windows/quickjs/libquickjs32.dll new file mode 100644 index 0000000..e39ca18 Binary files /dev/null and b/windows/quickjs/libquickjs32.dll differ diff --git a/windows/quickjs/libquickjs64.a b/windows/quickjs/libquickjs64.a new file mode 100644 index 0000000..9f375e7 Binary files /dev/null and b/windows/quickjs/libquickjs64.a differ diff --git a/windows/quickjs/libquickjs64.dll b/windows/quickjs/libquickjs64.dll new file mode 100644 index 0000000..84e7968 Binary files /dev/null and b/windows/quickjs/libquickjs64.dll differ diff --git a/windows/quickjs/quickjs/list.h b/windows/quickjs/quickjs/list.h new file mode 100644 index 0000000..0a1bc5a --- /dev/null +++ b/windows/quickjs/quickjs/list.h @@ -0,0 +1,100 @@ +/* + * Linux klist like system + * + * Copyright (c) 2016-2017 Fabrice Bellard + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#ifndef LIST_H +#define LIST_H + +#ifndef NULL +#include +#endif + +struct list_head { + struct list_head *prev; + struct list_head *next; +}; + +#define LIST_HEAD_INIT(el) { &(el), &(el) } + +/* return the pointer of type 'type *' containing 'el' as field 'member' */ +#define list_entry(el, type, member) \ + ((type *)((uint8_t *)(el) - offsetof(type, member))) + +static inline void init_list_head(struct list_head *head) +{ + head->prev = head; + head->next = head; +} + +/* insert 'el' between 'prev' and 'next' */ +static inline void __list_add(struct list_head *el, + struct list_head *prev, struct list_head *next) +{ + prev->next = el; + el->prev = prev; + el->next = next; + next->prev = el; +} + +/* add 'el' at the head of the list 'head' (= after element head) */ +static inline void list_add(struct list_head *el, struct list_head *head) +{ + __list_add(el, head, head->next); +} + +/* add 'el' at the end of the list 'head' (= before element head) */ +static inline void list_add_tail(struct list_head *el, struct list_head *head) +{ + __list_add(el, head->prev, head); +} + +static inline void list_del(struct list_head *el) +{ + struct list_head *prev, *next; + prev = el->prev; + next = el->next; + prev->next = next; + next->prev = prev; + el->prev = NULL; /* fail safe */ + el->next = NULL; /* fail safe */ +} + +static inline int list_empty(struct list_head *el) +{ + return el->next == el; +} + +#define list_for_each(el, head) \ + for(el = (head)->next; el != (head); el = el->next) + +#define list_for_each_safe(el, el1, head) \ + for(el = (head)->next, el1 = el->next; el != (head); \ + el = el1, el1 = el->next) + +#define list_for_each_prev(el, head) \ + for(el = (head)->prev; el != (head); el = el->prev) + +#define list_for_each_prev_safe(el, el1, head) \ + for(el = (head)->prev, el1 = el->prev; el != (head); \ + el = el1, el1 = el->prev) + +#endif /* LIST_H */ diff --git a/windows/quickjs/quickjs/quickjs-libc.h b/windows/quickjs/quickjs/quickjs-libc.h new file mode 100644 index 0000000..b105028 --- /dev/null +++ b/windows/quickjs/quickjs/quickjs-libc.h @@ -0,0 +1,58 @@ +/* + * QuickJS C library + * + * Copyright (c) 2017-2018 Fabrice Bellard + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#ifndef QUICKJS_LIBC_H +#define QUICKJS_LIBC_H + +#include +#include + +#include "quickjs.h" + +#ifdef __cplusplus +extern "C" { +#endif + +JSModuleDef *js_init_module_std(JSContext *ctx, const char *module_name); +JSModuleDef *js_init_module_os(JSContext *ctx, const char *module_name); +void js_std_add_helpers(JSContext *ctx, int argc, char **argv); +void js_std_loop(JSContext *ctx); +void js_std_init_handlers(JSRuntime *rt); +void js_std_free_handlers(JSRuntime *rt); +void js_std_dump_error(JSContext *ctx); +uint8_t *js_load_file(JSContext *ctx, size_t *pbuf_len, const char *filename); +int js_module_set_import_meta(JSContext *ctx, JSValueConst func_val, + JS_BOOL use_realpath, JS_BOOL is_main); +JSModuleDef *js_module_loader(JSContext *ctx, + const char *module_name, void *opaque); +void js_std_eval_binary(JSContext *ctx, const uint8_t *buf, size_t buf_len, + int flags); +void js_std_promise_rejection_tracker(JSContext *ctx, JSValueConst promise, + JSValueConst reason, + JS_BOOL is_handled, void *opaque); + +#ifdef __cplusplus +} /* extern "C" { */ +#endif + +#endif /* QUICKJS_LIBC_H */ diff --git a/windows/quickjs/quickjs/quickjs.h b/windows/quickjs/quickjs/quickjs.h new file mode 100644 index 0000000..cb475b2 --- /dev/null +++ b/windows/quickjs/quickjs/quickjs.h @@ -0,0 +1,1031 @@ +/* + * QuickJS Javascript Engine + * + * Copyright (c) 2017-2020 Fabrice Bellard + * Copyright (c) 2017-2020 Charlie Gordon + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#ifndef QUICKJS_H +#define QUICKJS_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#if defined(__GNUC__) || defined(__clang__) +#define js_likely(x) __builtin_expect(!!(x), 1) +#define js_unlikely(x) __builtin_expect(!!(x), 0) +#define js_force_inline inline __attribute__((always_inline)) +#define __js_printf_like(f, a) __attribute__((format(printf, f, a))) +#else +#define js_likely(x) (x) +#define js_unlikely(x) (x) +#define js_force_inline inline +#define __js_printf_like(a, b) +#endif + +#define JS_BOOL int + +typedef struct JSRuntime JSRuntime; +typedef struct JSContext JSContext; +typedef struct JSObject JSObject; +typedef struct JSClass JSClass; +typedef uint32_t JSClassID; +typedef uint32_t JSAtom; + +#if INTPTR_MAX >= INT64_MAX +#define JS_PTR64 +#define JS_PTR64_DEF(a) a +#else +#define JS_PTR64_DEF(a) +#endif + +#ifndef JS_PTR64 +#define JS_NAN_BOXING +#endif + +enum { + /* all tags with a reference count are negative */ + JS_TAG_FIRST = -11, /* first negative tag */ + JS_TAG_BIG_DECIMAL = -11, + JS_TAG_BIG_INT = -10, + JS_TAG_BIG_FLOAT = -9, + JS_TAG_SYMBOL = -8, + JS_TAG_STRING = -7, + JS_TAG_MODULE = -3, /* used internally */ + JS_TAG_FUNCTION_BYTECODE = -2, /* used internally */ + JS_TAG_OBJECT = -1, + + JS_TAG_INT = 0, + JS_TAG_BOOL = 1, + JS_TAG_NULL = 2, + JS_TAG_UNDEFINED = 3, + JS_TAG_UNINITIALIZED = 4, + JS_TAG_CATCH_OFFSET = 5, + JS_TAG_EXCEPTION = 6, + JS_TAG_FLOAT64 = 7, + /* any larger tag is FLOAT64 if JS_NAN_BOXING */ +}; + +typedef struct JSRefCountHeader { + int ref_count; +} JSRefCountHeader; + +#define JS_FLOAT64_NAN NAN + +#ifdef CONFIG_CHECK_JSVALUE +/* JSValue consistency : it is not possible to run the code in this + mode, but it is useful to detect simple reference counting + errors. It would be interesting to modify a static C analyzer to + handle specific annotations (clang has such annotations but only + for objective C) */ +typedef struct __JSValue *JSValue; +typedef const struct __JSValue *JSValueConst; + +#define JS_VALUE_GET_TAG(v) (int)((uintptr_t)(v) & 0xf) +/* same as JS_VALUE_GET_TAG, but return JS_TAG_FLOAT64 with NaN boxing */ +#define JS_VALUE_GET_NORM_TAG(v) JS_VALUE_GET_TAG(v) +#define JS_VALUE_GET_INT(v) (int)((intptr_t)(v) >> 4) +#define JS_VALUE_GET_BOOL(v) JS_VALUE_GET_INT(v) +#define JS_VALUE_GET_FLOAT64(v) (double)JS_VALUE_GET_INT(v) +#define JS_VALUE_GET_PTR(v) (void *)((intptr_t)(v) & ~0xf) + +#define JS_MKVAL(tag, val) (JSValue)(intptr_t)(((val) << 4) | (tag)) +#define JS_MKPTR(tag, p) (JSValue)((intptr_t)(p) | (tag)) + +#define JS_TAG_IS_FLOAT64(tag) ((unsigned)(tag) == JS_TAG_FLOAT64) + +#define JS_NAN JS_MKVAL(JS_TAG_FLOAT64, 1) + +static inline JSValue __JS_NewFloat64(JSContext *ctx, double d) +{ + return JS_MKVAL(JS_TAG_FLOAT64, (int)d); +} + +static inline JS_BOOL JS_VALUE_IS_NAN(JSValue v) +{ + return 0; +} + +#elif defined(JS_NAN_BOXING) + +typedef uint64_t JSValue; + +#define JSValueConst JSValue + +#define JS_VALUE_GET_TAG(v) (int)((v) >> 32) +#define JS_VALUE_GET_INT(v) (int)(v) +#define JS_VALUE_GET_BOOL(v) (int)(v) +#define JS_VALUE_GET_PTR(v) (void *)(intptr_t)(v) + +#define JS_MKVAL(tag, val) (((uint64_t)(tag) << 32) | (uint32_t)(val)) +#define JS_MKPTR(tag, ptr) (((uint64_t)(tag) << 32) | (uintptr_t)(ptr)) + +#define JS_FLOAT64_TAG_ADDEND (0x7ff80000 - JS_TAG_FIRST + 1) /* quiet NaN encoding */ + +static inline double JS_VALUE_GET_FLOAT64(JSValue v) +{ + union { + JSValue v; + double d; + } u; + u.v = v; + u.v += (uint64_t)JS_FLOAT64_TAG_ADDEND << 32; + return u.d; +} + +#define JS_NAN (0x7ff8000000000000 - ((uint64_t)JS_FLOAT64_TAG_ADDEND << 32)) + +static inline JSValue __JS_NewFloat64(JSContext *ctx, double d) +{ + union { + double d; + uint64_t u64; + } u; + JSValue v; + u.d = d; + /* normalize NaN */ + if (js_unlikely((u.u64 & 0x7fffffffffffffff) > 0x7ff0000000000000)) + v = JS_NAN; + else + v = u.u64 - ((uint64_t)JS_FLOAT64_TAG_ADDEND << 32); + return v; +} + +#define JS_TAG_IS_FLOAT64(tag) ((unsigned)((tag) - JS_TAG_FIRST) >= (JS_TAG_FLOAT64 - JS_TAG_FIRST)) + +/* same as JS_VALUE_GET_TAG, but return JS_TAG_FLOAT64 with NaN boxing */ +static inline int JS_VALUE_GET_NORM_TAG(JSValue v) +{ + uint32_t tag; + tag = JS_VALUE_GET_TAG(v); + if (JS_TAG_IS_FLOAT64(tag)) + return JS_TAG_FLOAT64; + else + return tag; +} + +static inline JS_BOOL JS_VALUE_IS_NAN(JSValue v) +{ + uint32_t tag; + tag = JS_VALUE_GET_TAG(v); + return tag == (JS_NAN >> 32); +} + +#else /* !JS_NAN_BOXING */ + +typedef union JSValueUnion { + int32_t int32; + double float64; + void *ptr; +} JSValueUnion; + +typedef struct JSValue { + JSValueUnion u; + int64_t tag; +} JSValue; + +#define JSValueConst JSValue + +#define JS_VALUE_GET_TAG(v) ((int32_t)(v).tag) +/* same as JS_VALUE_GET_TAG, but return JS_TAG_FLOAT64 with NaN boxing */ +#define JS_VALUE_GET_NORM_TAG(v) JS_VALUE_GET_TAG(v) +#define JS_VALUE_GET_INT(v) ((v).u.int32) +#define JS_VALUE_GET_BOOL(v) ((v).u.int32) +#define JS_VALUE_GET_FLOAT64(v) ((v).u.float64) +#define JS_VALUE_GET_PTR(v) ((v).u.ptr) + +// #define JS_MKVAL(tag, val) (JSValue){ (JSValueUnion){ .int32 = val }, tag } +#define JS_MKVAL(tag, val) JSValue { JSValueUnion { val }, tag } +#define JS_MKPTR(tag, p) (JSValue){ (JSValueUnion){ .ptr = p }, tag } + +#define JS_TAG_IS_FLOAT64(tag) ((unsigned)(tag) == JS_TAG_FLOAT64) + +#define JS_NAN (JSValue){ .u.float64 = JS_FLOAT64_NAN, JS_TAG_FLOAT64 } + +static inline JSValue __JS_NewFloat64(JSContext *ctx, double d) +{ + JSValue v; + v.tag = JS_TAG_FLOAT64; + v.u.float64 = d; + return v; +} + +static inline JS_BOOL JS_VALUE_IS_NAN(JSValue v) +{ + union { + double d; + uint64_t u64; + } u; + if (v.tag != JS_TAG_FLOAT64) + return 0; + u.d = v.u.float64; + return (u.u64 & 0x7fffffffffffffff) > 0x7ff0000000000000; +} + +#endif /* !JS_NAN_BOXING */ + +#define JS_VALUE_IS_BOTH_INT(v1, v2) ((JS_VALUE_GET_TAG(v1) | JS_VALUE_GET_TAG(v2)) == 0) +#define JS_VALUE_IS_BOTH_FLOAT(v1, v2) (JS_TAG_IS_FLOAT64(JS_VALUE_GET_TAG(v1)) && JS_TAG_IS_FLOAT64(JS_VALUE_GET_TAG(v2))) + +#define JS_VALUE_GET_OBJ(v) ((JSObject *)JS_VALUE_GET_PTR(v)) +#define JS_VALUE_GET_STRING(v) ((JSString *)JS_VALUE_GET_PTR(v)) +#define JS_VALUE_HAS_REF_COUNT(v) ((unsigned)JS_VALUE_GET_TAG(v) >= (unsigned)JS_TAG_FIRST) + +/* special values */ +#define JS_NULL JS_MKVAL(JS_TAG_NULL, 0) +#define JS_UNDEFINED JS_MKVAL(JS_TAG_UNDEFINED, 0) +#define JS_FALSE JS_MKVAL(JS_TAG_BOOL, 0) +#define JS_TRUE JS_MKVAL(JS_TAG_BOOL, 1) +#define JS_EXCEPTION JS_MKVAL(JS_TAG_EXCEPTION, 0) +#define JS_UNINITIALIZED JS_MKVAL(JS_TAG_UNINITIALIZED, 0) + +/* flags for object properties */ +#define JS_PROP_CONFIGURABLE (1 << 0) +#define JS_PROP_WRITABLE (1 << 1) +#define JS_PROP_ENUMERABLE (1 << 2) +#define JS_PROP_C_W_E (JS_PROP_CONFIGURABLE | JS_PROP_WRITABLE | JS_PROP_ENUMERABLE) +#define JS_PROP_LENGTH (1 << 3) /* used internally in Arrays */ +#define JS_PROP_TMASK (3 << 4) /* mask for NORMAL, GETSET, VARREF, AUTOINIT */ +#define JS_PROP_NORMAL (0 << 4) +#define JS_PROP_GETSET (1 << 4) +#define JS_PROP_VARREF (2 << 4) /* used internally */ +#define JS_PROP_AUTOINIT (3 << 4) /* used internally */ + +/* flags for JS_DefineProperty */ +#define JS_PROP_HAS_SHIFT 8 +#define JS_PROP_HAS_CONFIGURABLE (1 << 8) +#define JS_PROP_HAS_WRITABLE (1 << 9) +#define JS_PROP_HAS_ENUMERABLE (1 << 10) +#define JS_PROP_HAS_GET (1 << 11) +#define JS_PROP_HAS_SET (1 << 12) +#define JS_PROP_HAS_VALUE (1 << 13) + +/* throw an exception if false would be returned + (JS_DefineProperty/JS_SetProperty) */ +#define JS_PROP_THROW (1 << 14) +/* throw an exception if false would be returned in strict mode + (JS_SetProperty) */ +#define JS_PROP_THROW_STRICT (1 << 15) + +#define JS_PROP_NO_ADD (1 << 16) /* internal use */ +#define JS_PROP_NO_EXOTIC (1 << 17) /* internal use */ + +#define JS_DEFAULT_STACK_SIZE (256 * 1024) + +/* JS_Eval() flags */ +#define JS_EVAL_TYPE_GLOBAL (0 << 0) /* global code (default) */ +#define JS_EVAL_TYPE_MODULE (1 << 0) /* module code */ +#define JS_EVAL_TYPE_DIRECT (2 << 0) /* direct call (internal use) */ +#define JS_EVAL_TYPE_INDIRECT (3 << 0) /* indirect call (internal use) */ +#define JS_EVAL_TYPE_MASK (3 << 0) + +#define JS_EVAL_FLAG_STRICT (1 << 3) /* force 'strict' mode */ +#define JS_EVAL_FLAG_STRIP (1 << 4) /* force 'strip' mode */ +/* compile but do not run. The result is an object with a + JS_TAG_FUNCTION_BYTECODE or JS_TAG_MODULE tag. It can be executed + with JS_EvalFunction(). */ +#define JS_EVAL_FLAG_COMPILE_ONLY (1 << 5) +/* don't include the stack frames before this eval in the Error() backtraces */ +#define JS_EVAL_FLAG_BACKTRACE_BARRIER (1 << 6) + +typedef JSValue JSCFunction(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv); +typedef JSValue JSCFunctionMagic(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic); +typedef JSValue JSCFunctionData(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic, JSValue *func_data); + +typedef struct JSMallocState { + size_t malloc_count; + size_t malloc_size; + size_t malloc_limit; + void *opaque; /* user opaque */ +} JSMallocState; + +typedef struct JSMallocFunctions { + void *(*js_malloc)(JSMallocState *s, size_t size); + void (*js_free)(JSMallocState *s, void *ptr); + void *(*js_realloc)(JSMallocState *s, void *ptr, size_t size); + size_t (*js_malloc_usable_size)(const void *ptr); +} JSMallocFunctions; + +typedef struct JSGCObjectHeader JSGCObjectHeader; + +JSRuntime *JS_NewRuntime(void); +/* info lifetime must exceed that of rt */ +void JS_SetRuntimeInfo(JSRuntime *rt, const char *info); +void JS_SetMemoryLimit(JSRuntime *rt, size_t limit); +void JS_SetGCThreshold(JSRuntime *rt, size_t gc_threshold); +void JS_SetMaxStackSize(JSRuntime *rt, size_t stack_size); +JSRuntime *JS_NewRuntime2(const JSMallocFunctions *mf, void *opaque); +void JS_FreeRuntime(JSRuntime *rt); +void *JS_GetRuntimeOpaque(JSRuntime *rt); +void JS_SetRuntimeOpaque(JSRuntime *rt, void *opaque); +typedef void JS_MarkFunc(JSRuntime *rt, JSGCObjectHeader *gp); +void JS_MarkValue(JSRuntime *rt, JSValueConst val, JS_MarkFunc *mark_func); +void JS_RunGC(JSRuntime *rt); +JS_BOOL JS_IsLiveObject(JSRuntime *rt, JSValueConst obj); + +JSContext *JS_NewContext(JSRuntime *rt); +void JS_FreeContext(JSContext *s); +JSContext *JS_DupContext(JSContext *ctx); +void *JS_GetContextOpaque(JSContext *ctx); +void JS_SetContextOpaque(JSContext *ctx, void *opaque); +JSRuntime *JS_GetRuntime(JSContext *ctx); +void JS_SetClassProto(JSContext *ctx, JSClassID class_id, JSValue obj); +JSValue JS_GetClassProto(JSContext *ctx, JSClassID class_id); + +/* the following functions are used to select the intrinsic object to + save memory */ +JSContext *JS_NewContextRaw(JSRuntime *rt); +void JS_AddIntrinsicBaseObjects(JSContext *ctx); +void JS_AddIntrinsicDate(JSContext *ctx); +void JS_AddIntrinsicEval(JSContext *ctx); +void JS_AddIntrinsicStringNormalize(JSContext *ctx); +void JS_AddIntrinsicRegExpCompiler(JSContext *ctx); +void JS_AddIntrinsicRegExp(JSContext *ctx); +void JS_AddIntrinsicJSON(JSContext *ctx); +void JS_AddIntrinsicProxy(JSContext *ctx); +void JS_AddIntrinsicMapSet(JSContext *ctx); +void JS_AddIntrinsicTypedArrays(JSContext *ctx); +void JS_AddIntrinsicPromise(JSContext *ctx); +void JS_AddIntrinsicBigInt(JSContext *ctx); +void JS_AddIntrinsicBigFloat(JSContext *ctx); +void JS_AddIntrinsicBigDecimal(JSContext *ctx); +/* enable operator overloading */ +void JS_AddIntrinsicOperators(JSContext *ctx); +/* enable "use math" */ +void JS_EnableBignumExt(JSContext *ctx, JS_BOOL enable); + +JSValue js_string_codePointRange(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv); + +void *js_malloc_rt(JSRuntime *rt, size_t size); +void js_free_rt(JSRuntime *rt, void *ptr); +void *js_realloc_rt(JSRuntime *rt, void *ptr, size_t size); +size_t js_malloc_usable_size_rt(JSRuntime *rt, const void *ptr); +void *js_mallocz_rt(JSRuntime *rt, size_t size); + +void *js_malloc(JSContext *ctx, size_t size); +void js_free(JSContext *ctx, void *ptr); +void *js_realloc(JSContext *ctx, void *ptr, size_t size); +size_t js_malloc_usable_size(JSContext *ctx, const void *ptr); +void *js_realloc2(JSContext *ctx, void *ptr, size_t size, size_t *pslack); +void *js_mallocz(JSContext *ctx, size_t size); +char *js_strdup(JSContext *ctx, const char *str); +char *js_strndup(JSContext *ctx, const char *s, size_t n); + +typedef struct JSMemoryUsage { + int64_t malloc_size, malloc_limit, memory_used_size; + int64_t malloc_count; + int64_t memory_used_count; + int64_t atom_count, atom_size; + int64_t str_count, str_size; + int64_t obj_count, obj_size; + int64_t prop_count, prop_size; + int64_t shape_count, shape_size; + int64_t js_func_count, js_func_size, js_func_code_size; + int64_t js_func_pc2line_count, js_func_pc2line_size; + int64_t c_func_count, array_count; + int64_t fast_array_count, fast_array_elements; + int64_t binary_object_count, binary_object_size; +} JSMemoryUsage; + +void JS_ComputeMemoryUsage(JSRuntime *rt, JSMemoryUsage *s); +void JS_DumpMemoryUsage(FILE *fp, const JSMemoryUsage *s, JSRuntime *rt); + +/* atom support */ +JSAtom JS_NewAtomLen(JSContext *ctx, const char *str, size_t len); +JSAtom JS_NewAtom(JSContext *ctx, const char *str); +JSAtom JS_NewAtomUInt32(JSContext *ctx, uint32_t n); +JSAtom JS_DupAtom(JSContext *ctx, JSAtom v); +void JS_FreeAtom(JSContext *ctx, JSAtom v); +void JS_FreeAtomRT(JSRuntime *rt, JSAtom v); +JSValue JS_AtomToValue(JSContext *ctx, JSAtom atom); +JSValue JS_AtomToString(JSContext *ctx, JSAtom atom); +const char *JS_AtomToCString(JSContext *ctx, JSAtom atom); +JSAtom JS_ValueToAtom(JSContext *ctx, JSValueConst val); + +/* object class support */ + +typedef struct JSPropertyEnum { + JS_BOOL is_enumerable; + JSAtom atom; +} JSPropertyEnum; + +typedef struct JSPropertyDescriptor { + int flags; + JSValue value; + JSValue getter; + JSValue setter; +} JSPropertyDescriptor; + +typedef struct JSClassExoticMethods { + /* Return -1 if exception (can only happen in case of Proxy object), + FALSE if the property does not exists, TRUE if it exists. If 1 is + returned, the property descriptor 'desc' is filled if != NULL. */ + int (*get_own_property)(JSContext *ctx, JSPropertyDescriptor *desc, + JSValueConst obj, JSAtom prop); + /* '*ptab' should hold the '*plen' property keys. Return 0 if OK, + -1 if exception. The 'is_enumerable' field is ignored. + */ + int (*get_own_property_names)(JSContext *ctx, JSPropertyEnum **ptab, + uint32_t *plen, + JSValueConst obj); + /* return < 0 if exception, or TRUE/FALSE */ + int (*delete_property)(JSContext *ctx, JSValueConst obj, JSAtom prop); + /* return < 0 if exception or TRUE/FALSE */ + int (*define_own_property)(JSContext *ctx, JSValueConst this_obj, + JSAtom prop, JSValueConst val, + JSValueConst getter, JSValueConst setter, + int flags); + /* The following methods can be emulated with the previous ones, + so they are usually not needed */ + /* return < 0 if exception or TRUE/FALSE */ + int (*has_property)(JSContext *ctx, JSValueConst obj, JSAtom atom); + JSValue (*get_property)(JSContext *ctx, JSValueConst obj, JSAtom atom, + JSValueConst receiver); + /* return < 0 if exception or TRUE/FALSE */ + int (*set_property)(JSContext *ctx, JSValueConst obj, JSAtom atom, + JSValueConst value, JSValueConst receiver, int flags); +} JSClassExoticMethods; + +typedef void JSClassFinalizer(JSRuntime *rt, JSValue val); +typedef void JSClassGCMark(JSRuntime *rt, JSValueConst val, + JS_MarkFunc *mark_func); +#define JS_CALL_FLAG_CONSTRUCTOR (1 << 0) +typedef JSValue JSClassCall(JSContext *ctx, JSValueConst func_obj, + JSValueConst this_val, int argc, JSValueConst *argv, + int flags); + +typedef struct JSClassDef { + const char *class_name; + JSClassFinalizer *finalizer; + JSClassGCMark *gc_mark; + /* if call != NULL, the object is a function. If (flags & + JS_CALL_FLAG_CONSTRUCTOR) != 0, the function is called as a + constructor. In this case, 'this_val' is new.target. A + constructor call only happens if the object constructor bit is + set (see JS_SetConstructorBit()). */ + JSClassCall *call; + /* XXX: suppress this indirection ? It is here only to save memory + because only a few classes need these methods */ + JSClassExoticMethods *exotic; +} JSClassDef; + +JSClassID JS_NewClassID(JSClassID *pclass_id); +int JS_NewClass(JSRuntime *rt, JSClassID class_id, const JSClassDef *class_def); +int JS_IsRegisteredClass(JSRuntime *rt, JSClassID class_id); + +/* value handling */ + +static js_force_inline JSValue JS_NewBool(JSContext *ctx, JS_BOOL val) +{ + return JS_MKVAL(JS_TAG_BOOL, (val != 0)); +} + +static js_force_inline JSValue JS_NewInt32(JSContext *ctx, int32_t val) +{ + return JS_MKVAL(JS_TAG_INT, val); +} + +static js_force_inline JSValue JS_NewCatchOffset(JSContext *ctx, int32_t val) +{ + return JS_MKVAL(JS_TAG_CATCH_OFFSET, val); +} + +static js_force_inline JSValue JS_NewInt64(JSContext *ctx, int64_t val) +{ + JSValue v; + if (val == (int32_t)val) { + v = JS_NewInt32(ctx, val); + } else { + v = __JS_NewFloat64(ctx, val); + } + return v; +} + +static js_force_inline JSValue JS_NewUint32(JSContext *ctx, uint32_t val) +{ + JSValue v; + if (val <= 0x7fffffff) { + v = JS_NewInt32(ctx, val); + } else { + v = __JS_NewFloat64(ctx, val); + } + return v; +} + +JSValue JS_NewBigInt64(JSContext *ctx, int64_t v); +JSValue JS_NewBigUint64(JSContext *ctx, uint64_t v); + +static js_force_inline JSValue JS_NewFloat64(JSContext *ctx, double d) +{ + JSValue v; + int32_t val; + union { + double d; + uint64_t u; + } u, t; + u.d = d; + val = (int32_t)d; + t.d = val; + /* -0 cannot be represented as integer, so we compare the bit + representation */ + if (u.u == t.u) { + v = JS_MKVAL(JS_TAG_INT, val); + } else { + v = __JS_NewFloat64(ctx, d); + } + return v; +} + +static inline JS_BOOL JS_IsNumber(JSValueConst v) +{ + int tag = JS_VALUE_GET_TAG(v); + return tag == JS_TAG_INT || JS_TAG_IS_FLOAT64(tag); +} + +static inline JS_BOOL JS_IsBigInt(JSContext *ctx, JSValueConst v) +{ + int tag = JS_VALUE_GET_TAG(v); + return tag == JS_TAG_BIG_INT; +} + +static inline JS_BOOL JS_IsBigFloat(JSValueConst v) +{ + int tag = JS_VALUE_GET_TAG(v); + return tag == JS_TAG_BIG_FLOAT; +} + +static inline JS_BOOL JS_IsBigDecimal(JSValueConst v) +{ + int tag = JS_VALUE_GET_TAG(v); + return tag == JS_TAG_BIG_DECIMAL; +} + +static inline JS_BOOL JS_IsBool(JSValueConst v) +{ + return JS_VALUE_GET_TAG(v) == JS_TAG_BOOL; +} + +static inline JS_BOOL JS_IsNull(JSValueConst v) +{ + return JS_VALUE_GET_TAG(v) == JS_TAG_NULL; +} + +static inline JS_BOOL JS_IsUndefined(JSValueConst v) +{ + return JS_VALUE_GET_TAG(v) == JS_TAG_UNDEFINED; +} + +static inline JS_BOOL JS_IsException(JSValueConst v) +{ + return js_unlikely(JS_VALUE_GET_TAG(v) == JS_TAG_EXCEPTION); +} + +static inline JS_BOOL JS_IsUninitialized(JSValueConst v) +{ + return js_unlikely(JS_VALUE_GET_TAG(v) == JS_TAG_UNINITIALIZED); +} + +static inline JS_BOOL JS_IsString(JSValueConst v) +{ + return JS_VALUE_GET_TAG(v) == JS_TAG_STRING; +} + +static inline JS_BOOL JS_IsSymbol(JSValueConst v) +{ + return JS_VALUE_GET_TAG(v) == JS_TAG_SYMBOL; +} + +static inline JS_BOOL JS_IsObject(JSValueConst v) +{ + return JS_VALUE_GET_TAG(v) == JS_TAG_OBJECT; +} + +JSValue JS_Throw(JSContext *ctx, JSValue obj); +JSValue JS_GetException(JSContext *ctx); +JS_BOOL JS_IsError(JSContext *ctx, JSValueConst val); +void JS_ResetUncatchableError(JSContext *ctx); +JSValue JS_NewError(JSContext *ctx); +JSValue __js_printf_like(2, 3) JS_ThrowSyntaxError(JSContext *ctx, const char *fmt, ...); +JSValue __js_printf_like(2, 3) JS_ThrowTypeError(JSContext *ctx, const char *fmt, ...); +JSValue __js_printf_like(2, 3) JS_ThrowReferenceError(JSContext *ctx, const char *fmt, ...); +JSValue __js_printf_like(2, 3) JS_ThrowRangeError(JSContext *ctx, const char *fmt, ...); +JSValue __js_printf_like(2, 3) JS_ThrowInternalError(JSContext *ctx, const char *fmt, ...); +JSValue JS_ThrowOutOfMemory(JSContext *ctx); + +void __JS_FreeValue(JSContext *ctx, JSValue v); +static inline void JS_FreeValue(JSContext *ctx, JSValue v) +{ + if (JS_VALUE_HAS_REF_COUNT(v)) { + JSRefCountHeader *p = (JSRefCountHeader *)JS_VALUE_GET_PTR(v); + if (--p->ref_count <= 0) { + __JS_FreeValue(ctx, v); + } + } +} +void __JS_FreeValueRT(JSRuntime *rt, JSValue v); +static inline void JS_FreeValueRT(JSRuntime *rt, JSValue v) +{ + if (JS_VALUE_HAS_REF_COUNT(v)) { + JSRefCountHeader *p = (JSRefCountHeader *)JS_VALUE_GET_PTR(v); + if (--p->ref_count <= 0) { + __JS_FreeValueRT(rt, v); + } + } +} + +static inline JSValue JS_DupValue(JSContext *ctx, JSValueConst v) +{ + if (JS_VALUE_HAS_REF_COUNT(v)) { + JSRefCountHeader *p = (JSRefCountHeader *)JS_VALUE_GET_PTR(v); + p->ref_count++; + } + return (JSValue)v; +} + +static inline JSValue JS_DupValueRT(JSRuntime *rt, JSValueConst v) +{ + if (JS_VALUE_HAS_REF_COUNT(v)) { + JSRefCountHeader *p = (JSRefCountHeader *)JS_VALUE_GET_PTR(v); + p->ref_count++; + } + return (JSValue)v; +} + +int JS_ToBool(JSContext *ctx, JSValueConst val); /* return -1 for JS_EXCEPTION */ +int JS_ToInt32(JSContext *ctx, int32_t *pres, JSValueConst val); +static inline int JS_ToUint32(JSContext *ctx, uint32_t *pres, JSValueConst val) +{ + return JS_ToInt32(ctx, (int32_t*)pres, val); +} +int JS_ToInt64(JSContext *ctx, int64_t *pres, JSValueConst val); +int JS_ToIndex(JSContext *ctx, uint64_t *plen, JSValueConst val); +int JS_ToFloat64(JSContext *ctx, double *pres, JSValueConst val); +/* return an exception if 'val' is a Number */ +int JS_ToBigInt64(JSContext *ctx, int64_t *pres, JSValueConst val); +/* same as JS_ToInt64() but allow BigInt */ +int JS_ToInt64Ext(JSContext *ctx, int64_t *pres, JSValueConst val); + +JSValue JS_NewStringLen(JSContext *ctx, const char *str1, size_t len1); +JSValue JS_NewString(JSContext *ctx, const char *str); +JSValue JS_NewAtomString(JSContext *ctx, const char *str); +JSValue JS_ToString(JSContext *ctx, JSValueConst val); +JSValue JS_ToPropertyKey(JSContext *ctx, JSValueConst val); +const char *JS_ToCStringLen2(JSContext *ctx, size_t *plen, JSValueConst val1, JS_BOOL cesu8); +static inline const char *JS_ToCStringLen(JSContext *ctx, size_t *plen, JSValueConst val1) +{ + return JS_ToCStringLen2(ctx, plen, val1, 0); +} +static inline const char *JS_ToCString(JSContext *ctx, JSValueConst val1) +{ + return JS_ToCStringLen2(ctx, NULL, val1, 0); +} +void JS_FreeCString(JSContext *ctx, const char *ptr); + +JSValue JS_NewObjectProtoClass(JSContext *ctx, JSValueConst proto, JSClassID class_id); +JSValue JS_NewObjectClass(JSContext *ctx, int class_id); +JSValue JS_NewObjectProto(JSContext *ctx, JSValueConst proto); +JSValue JS_NewObject(JSContext *ctx); + +JS_BOOL JS_IsFunction(JSContext* ctx, JSValueConst val); +JS_BOOL JS_IsConstructor(JSContext* ctx, JSValueConst val); +JS_BOOL JS_SetConstructorBit(JSContext *ctx, JSValueConst func_obj, JS_BOOL val); + +JSValue JS_NewArray(JSContext *ctx); +int JS_IsArray(JSContext *ctx, JSValueConst val); + +JSValue JS_GetPropertyInternal(JSContext *ctx, JSValueConst obj, + JSAtom prop, JSValueConst receiver, + JS_BOOL throw_ref_error); +static js_force_inline JSValue JS_GetProperty(JSContext *ctx, JSValueConst this_obj, + JSAtom prop) +{ + return JS_GetPropertyInternal(ctx, this_obj, prop, this_obj, 0); +} +JSValue JS_GetPropertyStr(JSContext *ctx, JSValueConst this_obj, + const char *prop); +JSValue JS_GetPropertyUint32(JSContext *ctx, JSValueConst this_obj, + uint32_t idx); + +int JS_SetPropertyInternal(JSContext *ctx, JSValueConst this_obj, + JSAtom prop, JSValue val, + int flags); +static inline int JS_SetProperty(JSContext *ctx, JSValueConst this_obj, + JSAtom prop, JSValue val) +{ + return JS_SetPropertyInternal(ctx, this_obj, prop, val, JS_PROP_THROW); +} +int JS_SetPropertyUint32(JSContext *ctx, JSValueConst this_obj, + uint32_t idx, JSValue val); +int JS_SetPropertyInt64(JSContext *ctx, JSValueConst this_obj, + int64_t idx, JSValue val); +int JS_SetPropertyStr(JSContext *ctx, JSValueConst this_obj, + const char *prop, JSValue val); +int JS_HasProperty(JSContext *ctx, JSValueConst this_obj, JSAtom prop); +int JS_IsExtensible(JSContext *ctx, JSValueConst obj); +int JS_PreventExtensions(JSContext *ctx, JSValueConst obj); +int JS_DeleteProperty(JSContext *ctx, JSValueConst obj, JSAtom prop, int flags); +int JS_SetPrototype(JSContext *ctx, JSValueConst obj, JSValueConst proto_val); +JSValue JS_GetPrototype(JSContext *ctx, JSValueConst val); + +#define JS_GPN_STRING_MASK (1 << 0) +#define JS_GPN_SYMBOL_MASK (1 << 1) +#define JS_GPN_PRIVATE_MASK (1 << 2) +/* only include the enumerable properties */ +#define JS_GPN_ENUM_ONLY (1 << 4) +/* set theJSPropertyEnum.is_enumerable field */ +#define JS_GPN_SET_ENUM (1 << 5) + +int JS_GetOwnPropertyNames(JSContext *ctx, JSPropertyEnum **ptab, + uint32_t *plen, JSValueConst obj, int flags); +int JS_GetOwnProperty(JSContext *ctx, JSPropertyDescriptor *desc, + JSValueConst obj, JSAtom prop); + +JSValue JS_Call(JSContext *ctx, JSValueConst func_obj, JSValueConst this_obj, + int argc, JSValueConst *argv); +JSValue JS_Invoke(JSContext *ctx, JSValueConst this_val, JSAtom atom, + int argc, JSValueConst *argv); +JSValue JS_CallConstructor(JSContext *ctx, JSValueConst func_obj, + int argc, JSValueConst *argv); +JSValue JS_CallConstructor2(JSContext *ctx, JSValueConst func_obj, + JSValueConst new_target, + int argc, JSValueConst *argv); +JS_BOOL JS_DetectModule(const char *input, size_t input_len); +/* 'input' must be zero terminated i.e. input[input_len] = '\0'. */ +JSValue JS_Eval(JSContext *ctx, const char *input, size_t input_len, + const char *filename, int eval_flags); +JSValue JS_EvalFunction(JSContext *ctx, JSValue fun_obj); +JSValue JS_GetGlobalObject(JSContext *ctx); +int JS_IsInstanceOf(JSContext *ctx, JSValueConst val, JSValueConst obj); +int JS_DefineProperty(JSContext *ctx, JSValueConst this_obj, + JSAtom prop, JSValueConst val, + JSValueConst getter, JSValueConst setter, int flags); +int JS_DefinePropertyValue(JSContext *ctx, JSValueConst this_obj, + JSAtom prop, JSValue val, int flags); +int JS_DefinePropertyValueUint32(JSContext *ctx, JSValueConst this_obj, + uint32_t idx, JSValue val, int flags); +int JS_DefinePropertyValueStr(JSContext *ctx, JSValueConst this_obj, + const char *prop, JSValue val, int flags); +int JS_DefinePropertyGetSet(JSContext *ctx, JSValueConst this_obj, + JSAtom prop, JSValue getter, JSValue setter, + int flags); +void JS_SetOpaque(JSValue obj, void *opaque); +void *JS_GetOpaque(JSValueConst obj, JSClassID class_id); +void *JS_GetOpaque2(JSContext *ctx, JSValueConst obj, JSClassID class_id); + +/* 'buf' must be zero terminated i.e. buf[buf_len] = '\0'. */ +JSValue JS_ParseJSON(JSContext *ctx, const char *buf, size_t buf_len, + const char *filename); +#define JS_PARSE_JSON_EXT (1 << 0) /* allow extended JSON */ +JSValue JS_ParseJSON2(JSContext *ctx, const char *buf, size_t buf_len, + const char *filename, int flags); +JSValue JS_JSONStringify(JSContext *ctx, JSValueConst obj, + JSValueConst replacer, JSValueConst space0); + +typedef void JSFreeArrayBufferDataFunc(JSRuntime *rt, void *opaque, void *ptr); +JSValue JS_NewArrayBuffer(JSContext *ctx, uint8_t *buf, size_t len, + JSFreeArrayBufferDataFunc *free_func, void *opaque, + JS_BOOL is_shared); +JSValue JS_NewArrayBufferCopy(JSContext *ctx, const uint8_t *buf, size_t len); +void JS_DetachArrayBuffer(JSContext *ctx, JSValueConst obj); +uint8_t *JS_GetArrayBuffer(JSContext *ctx, size_t *psize, JSValueConst obj); +JSValue JS_GetTypedArrayBuffer(JSContext *ctx, JSValueConst obj, + size_t *pbyte_offset, + size_t *pbyte_length, + size_t *pbytes_per_element); +typedef struct { + void *(*sab_alloc)(void *opaque, size_t size); + void (*sab_free)(void *opaque, void *ptr); + void (*sab_dup)(void *opaque, void *ptr); + void *sab_opaque; +} JSSharedArrayBufferFunctions; +void JS_SetSharedArrayBufferFunctions(JSRuntime *rt, + const JSSharedArrayBufferFunctions *sf); + +JSValue JS_NewPromiseCapability(JSContext *ctx, JSValue *resolving_funcs); + +/* is_handled = TRUE means that the rejection is handled */ +typedef void JSHostPromiseRejectionTracker(JSContext *ctx, JSValueConst promise, + JSValueConst reason, + JS_BOOL is_handled, void *opaque); +void JS_SetHostPromiseRejectionTracker(JSRuntime *rt, JSHostPromiseRejectionTracker *cb, void *opaque); + +/* return != 0 if the JS code needs to be interrupted */ +typedef int JSInterruptHandler(JSRuntime *rt, void *opaque); +void JS_SetInterruptHandler(JSRuntime *rt, JSInterruptHandler *cb, void *opaque); +/* if can_block is TRUE, Atomics.wait() can be used */ +void JS_SetCanBlock(JSRuntime *rt, JS_BOOL can_block); + +typedef struct JSModuleDef JSModuleDef; + +/* return the module specifier (allocated with js_malloc()) or NULL if + exception */ +typedef char *JSModuleNormalizeFunc(JSContext *ctx, + const char *module_base_name, + const char *module_name, void *opaque); +typedef JSModuleDef *JSModuleLoaderFunc(JSContext *ctx, + const char *module_name, void *opaque); + +/* module_normalize = NULL is allowed and invokes the default module + filename normalizer */ +void JS_SetModuleLoaderFunc(JSRuntime *rt, + JSModuleNormalizeFunc *module_normalize, + JSModuleLoaderFunc *module_loader, void *opaque); +/* return the import.meta object of a module */ +JSValue JS_GetImportMeta(JSContext *ctx, JSModuleDef *m); +JSAtom JS_GetModuleName(JSContext *ctx, JSModuleDef *m); + +/* JS Job support */ + +typedef JSValue JSJobFunc(JSContext *ctx, int argc, JSValueConst *argv); +int JS_EnqueueJob(JSContext *ctx, JSJobFunc *job_func, int argc, JSValueConst *argv); + +JS_BOOL JS_IsJobPending(JSRuntime *rt); +int JS_ExecutePendingJob(JSRuntime *rt, JSContext **pctx); + +/* Object Writer/Reader (currently only used to handle precompiled code) */ +#define JS_WRITE_OBJ_BYTECODE (1 << 0) /* allow function/module */ +#define JS_WRITE_OBJ_BSWAP (1 << 1) /* byte swapped output */ +#define JS_WRITE_OBJ_SAB (1 << 2) /* allow SharedArrayBuffer */ +#define JS_WRITE_OBJ_REFERENCE (1 << 3) /* allow object references to + encode arbitrary object + graph */ +uint8_t *JS_WriteObject(JSContext *ctx, size_t *psize, JSValueConst obj, + int flags); +uint8_t *JS_WriteObject2(JSContext *ctx, size_t *psize, JSValueConst obj, + int flags, uint8_t ***psab_tab, size_t *psab_tab_len); + +#define JS_READ_OBJ_BYTECODE (1 << 0) /* allow function/module */ +#define JS_READ_OBJ_ROM_DATA (1 << 1) /* avoid duplicating 'buf' data */ +#define JS_READ_OBJ_SAB (1 << 2) /* allow SharedArrayBuffer */ +#define JS_READ_OBJ_REFERENCE (1 << 3) /* allow object references */ +JSValue JS_ReadObject(JSContext *ctx, const uint8_t *buf, size_t buf_len, + int flags); + +/* load the dependencies of the module 'obj'. Useful when JS_ReadObject() + returns a module. */ +int JS_ResolveModule(JSContext *ctx, JSValueConst obj); + +/* C function definition */ +typedef enum JSCFunctionEnum { /* XXX: should rename for namespace isolation */ + JS_CFUNC_generic, + JS_CFUNC_generic_magic, + JS_CFUNC_constructor, + JS_CFUNC_constructor_magic, + JS_CFUNC_constructor_or_func, + JS_CFUNC_constructor_or_func_magic, + JS_CFUNC_f_f, + JS_CFUNC_f_f_f, + JS_CFUNC_getter, + JS_CFUNC_setter, + JS_CFUNC_getter_magic, + JS_CFUNC_setter_magic, + JS_CFUNC_iterator_next, +} JSCFunctionEnum; + +typedef union JSCFunctionType { + JSCFunction *generic; + JSValue (*generic_magic)(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic); + JSCFunction *constructor; + JSValue (*constructor_magic)(JSContext *ctx, JSValueConst new_target, int argc, JSValueConst *argv, int magic); + JSCFunction *constructor_or_func; + double (*f_f)(double); + double (*f_f_f)(double, double); + JSValue (*getter)(JSContext *ctx, JSValueConst this_val); + JSValue (*setter)(JSContext *ctx, JSValueConst this_val, JSValueConst val); + JSValue (*getter_magic)(JSContext *ctx, JSValueConst this_val, int magic); + JSValue (*setter_magic)(JSContext *ctx, JSValueConst this_val, JSValueConst val, int magic); + JSValue (*iterator_next)(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv, int *pdone, int magic); +} JSCFunctionType; + +JSValue JS_NewCFunction2(JSContext *ctx, JSCFunction *func, + const char *name, + int length, JSCFunctionEnum cproto, int magic); +JSValue JS_NewCFunctionData(JSContext *ctx, JSCFunctionData *func, + int length, int magic, int data_len, + JSValueConst *data); + +static inline JSValue JS_NewCFunction(JSContext *ctx, JSCFunction *func, const char *name, + int length) +{ + return JS_NewCFunction2(ctx, func, name, length, JS_CFUNC_generic, 0); +} + +static inline JSValue JS_NewCFunctionMagic(JSContext *ctx, JSCFunctionMagic *func, + const char *name, + int length, JSCFunctionEnum cproto, int magic) +{ + return JS_NewCFunction2(ctx, (JSCFunction *)func, name, length, cproto, magic); +} +void JS_SetConstructor(JSContext *ctx, JSValueConst func_obj, + JSValueConst proto); + +/* C property definition */ + +typedef struct JSCFunctionListEntry { + const char *name; + uint8_t prop_flags; + uint8_t def_type; + int16_t magic; + union { + struct { + uint8_t length; /* XXX: should move outside union */ + uint8_t cproto; /* XXX: should move outside union */ + JSCFunctionType cfunc; + } func; + struct { + JSCFunctionType get; + JSCFunctionType set; + } getset; + struct { + const char *name; + int base; + } alias; + struct { + const struct JSCFunctionListEntry *tab; + int len; + } prop_list; + const char *str; + int32_t i32; + int64_t i64; + double f64; + } u; +} JSCFunctionListEntry; + +#define JS_DEF_CFUNC 0 +#define JS_DEF_CGETSET 1 +#define JS_DEF_CGETSET_MAGIC 2 +#define JS_DEF_PROP_STRING 3 +#define JS_DEF_PROP_INT32 4 +#define JS_DEF_PROP_INT64 5 +#define JS_DEF_PROP_DOUBLE 6 +#define JS_DEF_PROP_UNDEFINED 7 +#define JS_DEF_OBJECT 8 +#define JS_DEF_ALIAS 9 + +/* Note: c++ does not like nested designators */ +#define JS_CFUNC_DEF(name, length, func1) { name, JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE, JS_DEF_CFUNC, 0, .u = { .func = { length, JS_CFUNC_generic, { .generic = func1 } } } } +#define JS_CFUNC_MAGIC_DEF(name, length, func1, magic) { name, JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE, JS_DEF_CFUNC, magic, .u = { .func = { length, JS_CFUNC_generic_magic, { .generic_magic = func1 } } } } +#define JS_CFUNC_SPECIAL_DEF(name, length, cproto, func1) { name, JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE, JS_DEF_CFUNC, 0, .u = { .func = { length, JS_CFUNC_ ## cproto, { .cproto = func1 } } } } +#define JS_ITERATOR_NEXT_DEF(name, length, func1, magic) { name, JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE, JS_DEF_CFUNC, magic, .u = { .func = { length, JS_CFUNC_iterator_next, { .iterator_next = func1 } } } } +#define JS_CGETSET_DEF(name, fgetter, fsetter) { name, JS_PROP_CONFIGURABLE, JS_DEF_CGETSET, 0, .u = { .getset = { .get = { .getter = fgetter }, .set = { .setter = fsetter } } } } +#define JS_CGETSET_MAGIC_DEF(name, fgetter, fsetter, magic) { name, JS_PROP_CONFIGURABLE, JS_DEF_CGETSET_MAGIC, magic, .u = { .getset = { .get = { .getter_magic = fgetter }, .set = { .setter_magic = fsetter } } } } +#define JS_PROP_STRING_DEF(name, cstr, prop_flags) { name, prop_flags, JS_DEF_PROP_STRING, 0, .u = { .str = cstr } } +#define JS_PROP_INT32_DEF(name, val, prop_flags) { name, prop_flags, JS_DEF_PROP_INT32, 0, .u = { .i32 = val } } +#define JS_PROP_INT64_DEF(name, val, prop_flags) { name, prop_flags, JS_DEF_PROP_INT64, 0, .u = { .i64 = val } } +#define JS_PROP_DOUBLE_DEF(name, val, prop_flags) { name, prop_flags, JS_DEF_PROP_DOUBLE, 0, .u = { .f64 = val } } +#define JS_PROP_UNDEFINED_DEF(name, prop_flags) { name, prop_flags, JS_DEF_PROP_UNDEFINED, 0, .u = { .i32 = 0 } } +#define JS_OBJECT_DEF(name, tab, len, prop_flags) { name, prop_flags, JS_DEF_OBJECT, 0, .u = { .prop_list = { tab, len } } } +#define JS_ALIAS_DEF(name, from) { name, JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE, JS_DEF_ALIAS, 0, .u = { .alias = { from, -1 } } } +#define JS_ALIAS_BASE_DEF(name, from, base) { name, JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE, JS_DEF_ALIAS, 0, .u = { .alias = { from, base } } } + +void JS_SetPropertyFunctionList(JSContext *ctx, JSValueConst obj, + const JSCFunctionListEntry *tab, + int len); + +/* C module definition */ + +typedef int JSModuleInitFunc(JSContext *ctx, JSModuleDef *m); + +JSModuleDef *JS_NewCModule(JSContext *ctx, const char *name_str, + JSModuleInitFunc *func); +/* can only be called before the module is instantiated */ +int JS_AddModuleExport(JSContext *ctx, JSModuleDef *m, const char *name_str); +int JS_AddModuleExportList(JSContext *ctx, JSModuleDef *m, + const JSCFunctionListEntry *tab, int len); +/* can only be called after the module is instantiated */ +int JS_SetModuleExport(JSContext *ctx, JSModuleDef *m, const char *export_name, + JSValue val); +int JS_SetModuleExportList(JSContext *ctx, JSModuleDef *m, + const JSCFunctionListEntry *tab, int len); + +#undef js_unlikely +#undef js_force_inline + +#ifdef __cplusplus +} /* extern "C" { */ +#endif + +#endif /* QUICKJS_H */ diff --git a/windows/quickjs/quickjspp.hpp b/windows/quickjs/quickjspp.hpp new file mode 100644 index 0000000..72e2b18 --- /dev/null +++ b/windows/quickjs/quickjspp.hpp @@ -0,0 +1,1304 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace qjs { +#include "quickjs/quickjs.h" +#include "quickjs/quickjs-libc.h" + + +/** Exception type. + * Indicates that exception has occured in JS context. + */ +class exception {}; + +/** Javascript conversion traits. + * Describes how to convert type R to/from JSValue. Second template argument can be used for SFINAE/enable_if type filters. + */ +template +struct js_traits +{ + /** Create an object of C++ type R given JSValue v and JSContext. + * This function is intentionally not implemented. User should implement this function for their own type. + * @param v This value is passed as JSValueConst so it should be freed by the caller. + * @throws exception in case of conversion error + */ + static R unwrap(JSContext * ctx, JSValueConst v); + /** Create JSValue from an object of type R and JSContext. + * This function is intentionally not implemented. User should implement this function for their own type. + * @return Returns JSValue which should be freed by the caller or JS_EXCEPTION in case of error. + */ + static JSValue wrap(JSContext * ctx, R value); +}; + +/** Conversion traits for JSValue (identity). + */ +template <> +struct js_traits +{ + static JSValue unwrap(JSContext * ctx, JSValueConst v) noexcept + { + return JS_DupValue(ctx, v); + } + + static JSValue wrap(JSContext * ctx, JSValue v) noexcept + { + return v; + } +}; + +/** Conversion traits for integers. + */ +template +struct js_traits && sizeof(Int) <= sizeof(int64_t)>> +{ + + /// @throws exception + static Int unwrap(JSContext * ctx, JSValueConst v) + { + if constexpr (sizeof(Int) > sizeof(int32_t)) + { + int64_t r; + if(JS_ToInt64(ctx, &r, v)) + throw exception{}; + return static_cast(r); + } + else + { + int32_t r; + if(JS_ToInt32(ctx, &r, v)) + throw exception{}; + return static_cast(r); + } + } + + static JSValue wrap(JSContext * ctx, Int i) noexcept + { + if constexpr (std::is_same_v || sizeof(Int) > sizeof(int32_t)) + return JS_NewInt64(ctx, static_cast(i)); + else + return JS_NewInt32(ctx, static_cast(i)); + } +}; + +/** Conversion traits for boolean. + */ +template <> +struct js_traits +{ + static bool unwrap(JSContext * ctx, JSValueConst v) noexcept + { + return JS_ToBool(ctx, v); + } + + static JSValue wrap(JSContext * ctx, bool i) noexcept + { + return JS_NewBool(ctx, i); + } +}; + +/** Conversion trait for void. + */ +template <> +struct js_traits +{ + /// @throws exception if jsvalue is neither undefined nor null + static void unwrap(JSContext * ctx, JSValueConst value) + { + if(JS_IsException(value)) + throw exception{}; + } +}; + +/** Conversion traits for float64/double. + */ +template <> +struct js_traits +{ + /// @throws exception + static double unwrap(JSContext * ctx, JSValueConst v) + { + double r; + if(JS_ToFloat64(ctx, &r, v)) + throw exception{}; + return r; + } + + static JSValue wrap(JSContext * ctx, double i) noexcept + { + return JS_NewFloat64(ctx, i); + } +}; + +namespace detail { +/** Fake std::string_view which frees the string on destruction. +*/ +class js_string : public std::string_view +{ + using Base = std::string_view; + JSContext * ctx = nullptr; + + friend struct js_traits; + + js_string(JSContext * ctx, const char * ptr, std::size_t len) : Base(ptr, len), ctx(ctx) + {} + +public: + + template + js_string(Args&& ... args) : Base(std::forward(args)...), ctx(nullptr) + {} + + js_string(const js_string& other) = delete; + + operator const char * () const { + return this->data(); + } + + ~js_string() + { + if(ctx) + JS_FreeCString(ctx, this->data()); + } +}; +} // namespace detail + +/** Conversion traits from std::string_view and to detail::js_string. */ +template <> +struct js_traits +{ + static detail::js_string unwrap(JSContext * ctx, JSValueConst v) + { + size_t plen; + const char * ptr = JS_ToCStringLen(ctx, &plen, v); + if(!ptr) + throw exception{}; + return detail::js_string{ctx, ptr, plen}; + } + + static JSValue wrap(JSContext * ctx, std::string_view str) noexcept + { + return JS_NewStringLen(ctx, str.data(), str.size()); + } +}; + +/** Conversion traits for std::string */ +template <> // slower +struct js_traits +{ + static std::string unwrap(JSContext * ctx, JSValueConst v) + { + auto str_view = js_traits::unwrap(ctx, v); + return std::string{str_view.data(), str_view.size()}; + } + + static JSValue wrap(JSContext * ctx, const std::string& str) noexcept + { + return JS_NewStringLen(ctx, str.data(), str.size()); + } +}; + +/** Conversion from const char * */ +template <> +struct js_traits +{ + static JSValue wrap(JSContext * ctx, const char * str) noexcept + { + return JS_NewString(ctx, str); + } + static detail::js_string unwrap(JSContext * ctx, JSValueConst v) + { + return js_traits::unwrap(ctx, v); + } +}; + + +namespace detail { + +/** Helper function to convert and then free JSValue. */ +template +T unwrap_free(JSContext * ctx, JSValue val) +{ + if constexpr(std::is_same_v) + { + JS_FreeValue(ctx, val); + return js_traits::unwrap(ctx, val); + } else + { + try + { + T result = js_traits>::unwrap(ctx, val); + JS_FreeValue(ctx, val); + return result; + } + catch(...) + { + JS_FreeValue(ctx, val); + throw; + } + } +} + +template +Tuple unwrap_args_impl(JSContext * ctx, JSValueConst * argv, std::index_sequence) +{ + return Tuple{js_traits>>::unwrap(ctx, argv[I])...}; +} + +/** Helper function to convert an array of JSValues to a tuple. + * @tparam Args C++ types of the argv array + */ +template +std::tuple...> unwrap_args(JSContext * ctx, JSValueConst * argv) +{ + return unwrap_args_impl...>>(ctx, argv, std::make_index_sequence()); +} + +/** Helper function to call f with an array of JSValues. + * @tparam R return type of f + * @tparam Args argument types of f + * @tparam Callable type of f (inferred) + * @param ctx JSContext + * @param f callable object + * @param argv array of JSValue's + * @return converted return value of f or JS_NULL if f returns void + */ +template +JSValue wrap_call(JSContext * ctx, Callable&& f, JSValueConst * argv) noexcept +{ + try + { + if constexpr(std::is_same_v) + { + std::apply(std::forward(f), unwrap_args(ctx, argv)); + return JS_NULL; + } else + { + return js_traits>::wrap(ctx, + std::apply(std::forward(f), + unwrap_args(ctx, argv))); + } + } + catch(exception) + { + return JS_EXCEPTION; + } +} + +/** Same as wrap_call, but pass this_value as first argument. + * @tparam FirstArg type of this_value + */ +template +JSValue wrap_this_call(JSContext * ctx, Callable&& f, JSValueConst this_value, JSValueConst * argv) noexcept +{ + try + { + if constexpr(std::is_same_v) + { + std::apply(std::forward(f), std::tuple_cat(unwrap_args(ctx, &this_value), + unwrap_args(ctx, argv))); + return JS_NULL; + } else + { + return js_traits>::wrap(ctx, + std::apply(std::forward(f), + std::tuple_cat( + unwrap_args(ctx, &this_value), + unwrap_args(ctx, argv)))); + } + } + catch(exception) + { + return JS_EXCEPTION; + } +} + +template +void wrap_args_impl(JSContext * ctx, JSValue * argv, Tuple tuple, std::index_sequence) +{ + ((argv[I] = js_traits>>::wrap(ctx, std::get(tuple))), ...); +} + +/** Converts C++ args to JSValue array. + * @tparam Args argument types + * @param argv array of size at least sizeof...(Args) + */ +template +void wrap_args(JSContext * ctx, JSValue * argv, Args&& ... args) +{ + wrap_args_impl(ctx, argv, std::make_tuple(std::forward(args)...), + std::make_index_sequence()); +} +} // namespace detail + +/** A wrapper type for free and class member functions. + * Pointer to function F is a template argument. + * @tparam F either a pointer to free function or a pointer to class member function + * @tparam PassThis if true and F is a pointer to free function, passes Javascript "this" value as first argument: + */ +template +struct fwrapper +{ + /// "name" property of the JS function object (not defined if nullptr) + const char * name = nullptr; +}; + +/** Conversion to JSValue for free function in fwrapper. */ +template +struct js_traits> +{ + static JSValue wrap(JSContext * ctx, fwrapper fw) noexcept + { + return JS_NewCFunction(ctx, [](JSContext * ctx, JSValueConst this_value, int argc, + JSValueConst * argv) noexcept -> JSValue { + if constexpr(PassThis) + return detail::wrap_this_call(ctx, F, this_value, argv); + else + return detail::wrap_call(ctx, F, argv); + }, fw.name, sizeof...(Args)); + + } +}; + +/** Conversion to JSValue for class member function in fwrapper. */ +template +struct js_traits> +{ + static JSValue wrap(JSContext * ctx, fwrapper fw) noexcept + { + return JS_NewCFunction(ctx, [](JSContext * ctx, JSValueConst this_value, int argc, + JSValueConst * argv) noexcept -> JSValue { + return detail::wrap_this_call, Args...>(ctx, F, this_value, argv); + }, fw.name, sizeof...(Args)); + + } +}; + +/** Conversion to JSValue for const class member function in fwrapper. */ +template +struct js_traits> +{ + static JSValue wrap(JSContext * ctx, fwrapper fw) noexcept + { + return JS_NewCFunction(ctx, [](JSContext * ctx, JSValueConst this_value, int argc, + JSValueConst * argv) noexcept -> JSValue { + return detail::wrap_this_call, Args...>(ctx, F, this_value, argv); + }, fw.name, sizeof...(Args)); + + } +}; + +/** A wrapper type for constructor of type T with arguments Args. + * Compilation fails if no such constructor is defined. + * @tparam Args constructor arguments + */ +template +struct ctor_wrapper +{ + static_assert(std::is_constructible::value, "no such constructor!"); + /// "name" property of JS constructor object + const char * name = nullptr; +}; + +/** Conversion to JSValue for ctor_wrapper. */ +template +struct js_traits> +{ + static JSValue wrap(JSContext * ctx, ctor_wrapper cw) noexcept + { + return JS_NewCFunction2(ctx, [](JSContext * ctx, JSValueConst this_value, int argc, + JSValueConst * argv) noexcept -> JSValue { + + if(js_traits>::QJSClassId == 0) // not registered + { +#if defined(__cpp_rtti) + // automatically register class on first use (no prototype) + js_traits>::register_class(ctx, typeid(T).name()); +#else + JS_ThrowTypeError(ctx, "quickjspp ctor_wrapper::wrap: Class is not registered"); + return JS_EXCEPTION; +#endif + } + + auto proto = JS_GetPropertyStr(ctx, this_value, "prototype"); + if (JS_IsException(proto)) + return proto; + auto jsobj = JS_NewObjectProtoClass(ctx, proto, js_traits>::QJSClassId); + JS_FreeValue(ctx, proto); + if (JS_IsException(jsobj)) + return jsobj; + + std::shared_ptr ptr = std::apply(std::make_shared, detail::unwrap_args(ctx, argv)); + JS_SetOpaque(jsobj, new std::shared_ptr(std::move(ptr))); + return jsobj; + + // return detail::wrap_call, Args...>(ctx, std::make_shared, argv); + }, cw.name, sizeof...(Args), JS_CFUNC_constructor, 0); + } +}; + + +/** Conversions for std::shared_ptr. + * T should be registered to a context before conversions. + * @tparam T class type + */ +template +struct js_traits> +{ + /// Registered class id in QuickJS. + inline static JSClassID QJSClassId = 0; + + /** Register class in QuickJS context. + * + * @param ctx context + * @param name class name + * @param proto class prototype or JS_NULL + * @throws exception + */ + static void register_class(JSContext * ctx, const char * name, JSValue proto = JS_NULL) + { + if(QJSClassId == 0) + { + JS_NewClassID(&QJSClassId); + } + auto rt = JS_GetRuntime(ctx); + if(!JS_IsRegisteredClass(rt, QJSClassId)) + { + JSClassDef def{ + name, + // destructor + [](JSRuntime * rt, JSValue obj) noexcept { + auto pptr = reinterpret_cast *>(JS_GetOpaque(obj, QJSClassId)); + delete pptr; + } + }; + int e = JS_NewClass(rt, QJSClassId, &def); + if(e < 0) + { + JS_ThrowInternalError(ctx, "Cant register class %s", name); + throw exception{}; + } + } + JS_SetClassProto(ctx, QJSClassId, proto); + } + + /** Create a JSValue from std::shared_ptr. + * Creates an object with class if #QJSClassId and sets its opaque pointer to a new copy of #ptr. + */ + static JSValue wrap(JSContext * ctx, std::shared_ptr ptr) + { + if(QJSClassId == 0) // not registered + { +#if defined(__cpp_rtti) + // automatically register class on first use (no prototype) + register_class(ctx, typeid(T).name()); +#else + JS_ThrowTypeError(ctx, "quickjspp std::shared_ptr::wrap: Class is not registered"); + return JS_EXCEPTION; +#endif + } + auto jsobj = JS_NewObjectClass(ctx, QJSClassId); + if(JS_IsException(jsobj)) + return jsobj; + + auto pptr = new std::shared_ptr(std::move(ptr)); + JS_SetOpaque(jsobj, pptr); + return jsobj; + } + + /// @throws exception if #v doesn't have the correct class id + static const std::shared_ptr& unwrap(JSContext * ctx, JSValueConst v) + { + auto ptr = reinterpret_cast *>(JS_GetOpaque2(ctx, v, QJSClassId)); + if(!ptr) + throw exception{}; + return *ptr; + } +}; + +// T * - non-owning pointer +template +struct js_traits +{ + static JSValue wrap(JSContext * ctx, T * ptr) + { + if(js_traits>::QJSClassId == 0) // not registered + { +#if defined(__cpp_rtti) + js_traits>::register_class(ctx, typeid(T).name()); +#else + JS_ThrowTypeError(ctx, "quickjspp js_traits::wrap: Class is not registered"); + return JS_EXCEPTION; +#endif + } + auto jsobj = JS_NewObjectClass(ctx, js_traits>::QJSClassId); + if(JS_IsException(jsobj)) + return jsobj; + + // shared_ptr with empty deleter since we don't own T* + auto pptr = new std::shared_ptr(ptr, [](T *){}); + JS_SetOpaque(jsobj, pptr); + return jsobj; + } + + static T * unwrap(JSContext * ctx, JSValueConst v) + { + auto ptr = reinterpret_cast *>(JS_GetOpaque2(ctx, v, + js_traits>::QJSClassId)); + if(!ptr) + throw exception{}; + return ptr->get(); + } +}; + +namespace detail { +/** A faster std::function-like object with type erasure. + * Used to convert any callable objects (including lambdas) to JSValue. + */ +struct function +{ + JSValue + (* invoker)(function * self, JSContext * ctx, JSValueConst this_value, int argc, JSValueConst * argv) = nullptr; + + void (* destroyer)(function * self) = nullptr; + + alignas(std::max_align_t) char functor[]; + + template + static function * create(JSRuntime * rt, Functor&& f) + { + auto fptr = reinterpret_cast(js_malloc_rt(rt, sizeof(function) + sizeof(Functor))); + if(!fptr) + throw std::bad_alloc{}; + new(fptr) function; + auto functorptr = reinterpret_cast(fptr->functor); + new(functorptr) Functor(std::forward(f)); + fptr->destroyer = nullptr; + if constexpr(!std::is_trivially_destructible_v) + { + fptr->destroyer = [](function * fptr) { + auto functorptr = reinterpret_cast(fptr->functor); + functorptr->~Functor(); + }; + } + return fptr; + } +}; + +static_assert(std::is_trivially_destructible_v); +} + +template <> +struct js_traits +{ + inline static JSClassID QJSClassId = 0; + + // TODO: replace ctx with rt + static void register_class(JSContext * ctx, const char * name) + { + if(QJSClassId == 0) + { + JS_NewClassID(&QJSClassId); + } + auto rt = JS_GetRuntime(ctx); + if(JS_IsRegisteredClass(rt, QJSClassId)) + return; + JSClassDef def{ + name, + // destructor + [](JSRuntime * rt, JSValue obj) noexcept { + auto fptr = reinterpret_cast(JS_GetOpaque(obj, QJSClassId)); + assert(fptr); + if(fptr->destroyer) + fptr->destroyer(fptr); + js_free_rt(rt, fptr); + }, + nullptr, // mark + // call + [](JSContext * ctx, JSValueConst func_obj, JSValueConst this_val, int argc, + JSValueConst * argv, int flags) -> JSValue { + auto ptr = reinterpret_cast(JS_GetOpaque2(ctx, func_obj, QJSClassId)); + if(!ptr) + return JS_EXCEPTION; + return ptr->invoker(ptr, ctx, this_val, argc, argv); + } + }; + int e = JS_NewClass(rt, QJSClassId, &def); + if(e < 0) + throw std::runtime_error{"Cannot register C++ function class"}; + } +}; + + +/** Traits for accessing object properties. + * @tparam Key property key type (uint32 and strings are supported) + */ +template +struct js_property_traits +{ + static void set_property(JSContext * ctx, JSValue this_obj, Key key, JSValue value); + static JSValue get_property(JSContext * ctx, JSValue this_obj, Key key); +}; + +template <> +struct js_property_traits +{ + static void set_property(JSContext * ctx, JSValue this_obj, const char * name, JSValue value) + { + int err = JS_SetPropertyStr(ctx, this_obj, name, value); + if(err < 0) + throw exception{}; + } + + static JSValue get_property(JSContext * ctx, JSValue this_obj, const char * name) noexcept + { + return JS_GetPropertyStr(ctx, this_obj, name); + } +}; + +template <> +struct js_property_traits +{ + static void set_property(JSContext * ctx, JSValue this_obj, uint32_t idx, JSValue value) + { + int err = JS_SetPropertyUint32(ctx, this_obj, idx, value); + if(err < 0) + throw exception{}; + } + + static JSValue get_property(JSContext * ctx, JSValue this_obj, uint32_t idx) noexcept + { + return JS_GetPropertyUint32(ctx, this_obj, idx); + } +}; + +class Value; + +namespace detail { +template +struct property_proxy +{ + JSContext * ctx; + JSValue this_obj; + Key key; + + /** Conversion helper function */ + template + T as() const + { + return unwrap_free(ctx, js_property_traits::get_property(ctx, this_obj, key)); + } + + /** Explicit conversion operator (to any type) */ + template + explicit operator T() const { return as(); } + + /** Implicit converion to qjs::Value */ + operator Value() const; // defined later due to Value being incomplete type + + template + property_proxy& operator =(Value value) + { + js_property_traits::set_property(ctx, this_obj, key, + js_traits::wrap(ctx, std::move(value))); + return *this; + } +}; + + +// class member variable getter/setter +template +struct get_set {}; + +template +struct get_set +{ + using is_const = std::is_const; + + static const R& get(const std::shared_ptr& ptr) + { + return *ptr.*M; + } + + static R& set(const std::shared_ptr& ptr, R value) + { + return *ptr.*M = std::move(value); + } + +}; + +} // namespace detail + +/** JSValue with RAAI semantics. + * A wrapper over (JSValue v, JSContext * ctx). + * Calls JS_FreeValue(ctx, v) on destruction. Can be copied and moved. + * A JSValue can be released by either JSValue x = std::move(value); or JSValue x = value.release(), then the Value becomes invalid and FreeValue won't be called + * Can be converted to C++ type, for example: auto string = value.as(); qjs::exception would be thrown on error + * Properties can be accessed (read/write): value["property1"] = 1; value[2] = "2"; + */ +class Value +{ +public: + JSValue v; + JSContext * ctx = nullptr; + +public: + /** Use context.newValue(val) instead */ + template + Value(JSContext * ctx, T&& val) : ctx(ctx) + { + v = js_traits>::wrap(ctx, std::forward(val)); + if(JS_IsException(v)) + throw exception{}; + } + + Value(const Value& rhs) + { + ctx = rhs.ctx; + v = JS_DupValue(ctx, rhs.v); + } + + Value(Value&& rhs) + { + std::swap(ctx, rhs.ctx); + v = rhs.v; + } + + Value& operator=(Value rhs) + { + std::swap(ctx, rhs.ctx); + std::swap(v, rhs.v); + return *this; + } + + bool operator==(JSValueConst other) const + { + return JS_VALUE_GET_TAG(v) == JS_VALUE_GET_TAG(other) && JS_VALUE_GET_PTR(v) == JS_VALUE_GET_PTR(other); + } + + bool operator!=(JSValueConst other) const { return !((*this) == other); } + + + /** Returns true if 2 values are the same (equality for arithmetic types or point to the same object) */ + bool operator==(const Value& rhs) const + { + return ctx == rhs.ctx && (*this == rhs.v); + } + + bool operator!=(const Value& rhs) const { return !((*this) == rhs); } + + + ~Value() + { + if(ctx) JS_FreeValue(ctx, v); + } + + bool isError() const { return JS_IsError(ctx, v); } + + /** Conversion helper function. Both value.as() and static_cast(value) are supported */ + template + T as() const { return js_traits>::unwrap(ctx, v); } + + /** Explicit conversion to any type */ + template + explicit operator T() const { return as(); } + + JSValue release() // dont call freevalue + { + ctx = nullptr; + return v; + } + + /** Implicit conversion to JSValue (rvalue only). Example: JSValue v = std::move(value); */ + operator JSValue() && { return release(); } + + + /** Access JS properties. Returns proxy type which is implicitly convertible to qjs::Value */ + template + detail::property_proxy operator [](Key key) + { + return {ctx, v, std::move(key)}; + } + + + // add("f", []() {...}); + template + Value& add(const char * name, Function&& f) + { + (*this)[name] = js_traits(f)})>::wrap(ctx, + std::forward(f)); + return *this; + } + + // add<&f>("f"); + // add<&T::f>("f"); + template + std::enable_if_t, Value&> + add(const char * name) + { + (*this)[name] = fwrapper{name}; + return *this; + } + + // add<&T::member>("member"); + template + std::enable_if_t, Value&> + add(const char * name) + { + auto prop = JS_NewAtom(ctx, name); + using fgetter = fwrapper::get, true>; + int ret; + if constexpr (detail::get_set::is_const::value) + { + ret = JS_DefinePropertyGetSet(ctx, v, prop, + js_traits::wrap(ctx, fgetter{name}), + JS_UNDEFINED, + JS_PROP_CONFIGURABLE | JS_PROP_ENUMERABLE + ); + } else + { + using fsetter = fwrapper::set, true>; + ret = JS_DefinePropertyGetSet(ctx, v, prop, + js_traits::wrap(ctx, fgetter{name}), + js_traits::wrap(ctx, fsetter{name}), + JS_PROP_CONFIGURABLE | JS_PROP_WRITABLE | JS_PROP_ENUMERABLE + ); + } + JS_FreeAtom(ctx, prop); + if(ret < 0) + throw exception{}; + return *this; + } + + std::string toJSON(const Value& replacer = Value{nullptr, JS_UNDEFINED}, const Value& space = Value{nullptr, JS_UNDEFINED}) + { + assert(ctx); + assert(!replacer.ctx || ctx == replacer.ctx); + assert(!space.ctx || ctx == space.ctx); + JSValue json = JS_JSONStringify(ctx, v, replacer.v, space.v); + return (std::string)Value{ctx, json}; + } + +}; + +/** Thin wrapper over JSRuntime * rt + * Calls JS_FreeRuntime on destruction. noncopyable. + */ +class Runtime +{ +public: + JSRuntime * rt; + + Runtime() + { + rt = JS_NewRuntime(); + if(!rt) + throw std::runtime_error{"qjs: Cannot create runtime"}; + } + + // noncopyable + Runtime(const Runtime&) = delete; + + ~Runtime() + { + JS_FreeRuntime(rt); + } +}; + +/** Wrapper over JSContext * ctx + * Calls JS_SetContextOpaque(ctx, this); on construction and JS_FreeContext on destruction + */ +class Context +{ +public: + JSContext * ctx; + + /** Module wrapper + * Workaround for lack of opaque pointer for module load function by keeping a list of modules in qjs::Context. + */ + class Module + { + friend class Context; + + JSModuleDef * m; + JSContext * ctx; + const char * name; + + using nvp = std::pair; + std::vector exports; + public: + Module(JSContext * ctx, const char * name) : ctx(ctx), name(name) + { + m = JS_NewCModule(ctx, name, [](JSContext * ctx, JSModuleDef * m) noexcept { + auto& context = Context::get(ctx); + auto it = std::find_if(context.modules.begin(), context.modules.end(), + [m](const Module& module) { return module.m == m; }); + if(it == context.modules.end()) + return -1; + for(const auto& e : it->exports) + { + if(JS_SetModuleExport(ctx, m, e.first, JS_DupValue(ctx, e.second.v)) != 0) + return -1; + } + return 0; + }); + if(!m) + throw exception{}; + } + + Module& add(const char * name, JSValue value) + { + exports.push_back({name, {ctx, value}}); + JS_AddModuleExport(ctx, m, name); + return *this; + } + + Module& add(const char * name, Value value) + { + assert(value.ctx == ctx); + exports.push_back({name, std::move(value)}); + JS_AddModuleExport(ctx, m, name); + return *this; + } + + template + Module& add(const char * name, T value) + { + return add(name, js_traits::wrap(ctx, std::move(value))); + } + + Module(const Module&) = delete; + + Module(Module&&) = default; + //Module& operator=(Module&&) = default; + + + // function wrappers + + /** Add free function F. + * Example: + * module.function(&::sin)>("sin"); + */ + template + Module& function(const char * name) + { + return add(name, qjs::fwrapper{name}); + } + + /** Add function object f. + * Slower than template version. + * Example: module.function("sin", [](double x) { return ::sin(x); }); + */ + template + Module& function(const char * name, F&& f) + { + return add(name, js_traits(f)})>::wrap(std::forward(f))); + } + + // class register wrapper + private: + /** Helper class to register class members and constructors. + * See fun, constructor. + * Actual registration occurs at object destruction. + */ + template + class class_registrar + { + const char * name; + qjs::Value prototype; + qjs::Context::Module& module; + qjs::Context& context; + public: + explicit class_registrar(const char * name, qjs::Context::Module& module, qjs::Context& context) : + name(name), + prototype(context.newObject()), + module(module), + context(context) + { + } + + class_registrar(const class_registrar&) = delete; + + /** Add functional object f + */ + template + class_registrar& fun(const char * name, F&& f) + { + prototype.add(name, std::forward(f)); + return *this; + } + + /** Add class member function or class member variable F + * Example: + * struct T { int var; int func(); } + * auto& module = context.addModule("module"); + * module.class_("T").fun<&T::var>("var").fun<&T::func>("func"); + */ + template + class_registrar& fun(const char * name) + { + prototype.add(name); + return *this; + } + + /** Add class constructor + * @tparam Args contructor arguments + * @param name constructor name (if not specified class name will be used) + */ + template + class_registrar& constructor(const char * name = nullptr) + { + if(!name) + name = this->name; + Value ctor = context.newValue(qjs::ctor_wrapper{name}); + JS_SetConstructor(context.ctx, ctor.v, prototype.v); + module.add(name, std::move(ctor)); + return *this; + } + + /* TODO: needs casting to base class + template + class_registrar& base() + { + assert(js_traits>::QJSClassId && "base class is not registered"); + auto base_proto = JS_GetClassProto(context.ctx, js_traits>::QJSClassId); + int err = JS_SetPrototype(context.ctx, prototype.v, base_proto); + JS_FreeValue(context.ctx, base_proto); + if(err < 0) + throw exception{}; + return *this; + } + */ + + ~class_registrar() + { + context.registerClass(name, std::move(prototype)); + } + }; + + public: + /** Add class to module. + * See \ref class_registrar. + */ + template + class_registrar class_(const char * name) + { + return class_registrar{name, *this, qjs::Context::get(ctx)}; + } + + }; + + std::vector modules; +private: + void init() + { + JS_SetContextOpaque(ctx, this); + js_traits::register_class(ctx, "C++ function"); + } + +public: + Context(Runtime& rt) : Context(rt.rt) + {} + + Context(JSRuntime * rt) + { + ctx = JS_NewContext(rt); + if(!ctx) + throw std::runtime_error{"qjs: Cannot create context"}; + init(); + } + + Context(JSContext * ctx) : ctx{ctx} + { + init(); + } + + // noncopyable + Context(const Context&) = delete; + + ~Context() + { + modules.clear(); + JS_FreeContext(ctx); + } + + /** Create module and return a reference to it */ + Module& addModule(const char * name) + { + modules.emplace_back(ctx, name); + return modules.back(); + } + + /** returns globalThis */ + Value global() { return Value{ctx, JS_GetGlobalObject(ctx)}; } + + /** returns new Object() */ + Value newObject() { return Value{ctx, JS_NewObject(ctx)}; } + + /** returns JS value converted from c++ object val */ + template + Value newValue(T&& val) { return Value{ctx, std::forward(val)}; } + + /** returns current exception associated with context, and resets it. Should be called when qjs::exception is caught */ + Value getException() { return Value{ctx, JS_GetException(ctx)}; } + + /** Register class T for conversions to/from std::shared_ptr to work. + * Wherever possible module.class_("T")... should be used instead. + * @tparam T class type + * @param name class name in JS engine + * @param proto JS class prototype or JS_UNDEFINED + */ + template + void registerClass(const char * name, JSValue proto = JS_NULL) + { + js_traits>::register_class(ctx, name, proto); + } + + Value eval(std::string_view buffer, const char * filename = "", unsigned eval_flags = 0) + { + assert(buffer.data()[buffer.size()] == '\0' && "eval buffer is not null-terminated"); // JS_Eval requirement + JSValue v = JS_Eval(ctx, buffer.data(), buffer.size(), filename, eval_flags); + return Value{ctx, v}; + } + + Value evalFile(const char * filename, unsigned eval_flags = 0) + { + size_t buf_len; + auto deleter = [this](void * p) { js_free(ctx, p); }; + auto buf = std::unique_ptr{js_load_file(ctx, &buf_len, filename), deleter}; + if(!buf) + throw std::runtime_error{std::string{"evalFile: can't read file: "} + filename}; + return eval({reinterpret_cast(buf.get()), buf_len}, filename, eval_flags); + } + + Value fromJSON(std::string_view buffer, const char * filename = "") + { + assert(buffer.data()[buffer.size()] == '\0' && "fromJSON buffer is not null-terminated"); // JS_ParseJSON requirement + JSValue v = JS_ParseJSON(ctx, buffer.data(), buffer.size(), filename); + return Value{ctx, v}; + } + + /** Get qjs::Context from JSContext opaque pointer */ + static Context& get(JSContext * ctx) + { + void * ptr = JS_GetContextOpaque(ctx); + assert(ptr); + return *reinterpret_cast(ptr); + } +}; + +/** Conversion traits for Value. + */ +template <> +struct js_traits +{ + static Value unwrap(JSContext * ctx, JSValueConst v) + { + return Value{ctx, JS_DupValue(ctx, v)}; + } + + static JSValue wrap(JSContext * ctx, Value v) noexcept + { + assert(ctx == v.ctx); + return v.release(); + } +}; + +/** Convert to/from std::function + * @tparam R return type + * @tparam Args argument types + */ +template +struct js_traits> +{ + static std::function unwrap(JSContext * ctx, JSValueConst fun_obj) + { + return [jsfun_obj = Value{ctx, JS_DupValue(ctx, fun_obj)}](Args&& ... args) -> R { + const int argc = sizeof...(Args); + JSValue argv[argc]; + detail::wrap_args(jsfun_obj.ctx, argv, std::forward(args)...); + JSValue result = JS_Call(jsfun_obj.ctx, jsfun_obj.v, JS_UNDEFINED, argc, const_cast(argv)); + for(int i = 0; i < argc; i++) JS_FreeValue(jsfun_obj.ctx, argv[i]); + return detail::unwrap_free(jsfun_obj.ctx, result); + }; + } + + /** Convert from function object functor to JSValue. + * Uses detail::function for type-erasure. + */ + template + static JSValue wrap(JSContext * ctx, Functor&& functor) + { + using detail::function; + assert(js_traits::QJSClassId); + auto obj = JS_NewObjectClass(ctx, js_traits::QJSClassId); + if(JS_IsException(obj)) + return JS_EXCEPTION; + auto fptr = function::create(JS_GetRuntime(ctx), std::forward(functor)); + fptr->invoker = [](function * self, JSContext * ctx, JSValueConst this_value, int argc, JSValueConst * argv) { + assert(self); + auto f = reinterpret_cast(&self->functor); + return detail::wrap_call(ctx, *f, argv); + }; + JS_SetOpaque(obj, fptr); + return obj; + } +}; + +/** Convert from std::vector to Array and vice-versa. If Array holds objects that are non-convertible to T throws qjs::exception */ +template +struct js_traits> +{ + static JSValue wrap(JSContext * ctx, const std::vector& arr) noexcept + { + auto jsarray = Value{ctx, JS_NewArray(ctx)}; + if(JS_IsException(jsarray.v)) + return jsarray.v; + + try + { + for(uint32_t i = 0; i < (uint32_t) arr.size(); i++) + jsarray[i] = arr[i]; + } + catch(exception) + { + return JS_EXCEPTION; + } + return std::move(jsarray); + } + + static std::vector unwrap(JSContext * ctx, JSValueConst jsarr) + { + int e = JS_IsArray(ctx, jsarr); + if(e == 0) + JS_ThrowTypeError(ctx, "js_traits>::unwrap expects array"); + if(e <= 0) + throw exception{}; + Value jsarray{ctx, JS_DupValue(ctx, jsarr)}; + std::vector arr; + auto len = static_cast(jsarray["length"]); + arr.reserve((uint32_t) len); + for(uint32_t i = 0; i < (uint32_t) len; i++) + arr.push_back(static_cast(jsarray[i])); + return arr; + } +}; + +namespace detail { +template +property_proxy::operator Value() const +{ + return as(); +} +} + +} // namespace qjs \ No newline at end of file