├── .github └── workflows │ └── publish.yml ├── .gitignore ├── .gitmodules ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── CMakeLists.txt ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── whl │ │ │ └── quickjs │ │ │ └── wrapper │ │ │ └── sample │ │ │ └── MainActivity.java │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── values-night │ │ └── themes.xml │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ └── test │ └── java │ └── com │ └── whl │ └── quickjs │ └── wrapper │ └── sample │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images ├── alipay.jpg └── wechat.png ├── native └── cpp │ ├── quickjs_context_jni.cpp │ ├── quickjs_extend_libraries.h │ ├── quickjs_wrapper.cpp │ └── quickjs_wrapper.h ├── remarks.md ├── settings.gradle ├── wrapper-android ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── whl │ │ └── quickjs │ │ └── wrapper │ │ ├── QuickJSCompileTest.java │ │ ├── QuickJSFreeValueTest.java │ │ └── QuickJSTest.java │ └── main │ ├── AndroidManifest.xml │ ├── CMakeLists.txt │ ├── assets │ ├── test_assert_define.js │ ├── test_base_module1.mjs │ ├── test_base_module2.mjs │ ├── test_module_import_dynamic.js │ └── test_polyfill_date.js │ ├── cpp │ └── quickjs_android.cpp │ └── java │ └── com │ └── whl │ └── quickjs │ └── android │ └── QuickJSLoader.java ├── wrapper-java ├── .gitignore ├── README.md ├── build.gradle └── src │ └── main │ ├── CMakeLists.txt │ └── java │ └── com │ └── whl │ └── quickjs │ └── wrapper │ ├── JSArray.java │ ├── JSCallFunction.java │ ├── JSFunction.java │ ├── JSMethod.java │ ├── JSObject.java │ ├── JSObjectCreator.java │ ├── MapCreator.java │ ├── MapFilter.java │ ├── ModuleLoader.java │ ├── QuickJSArray.java │ ├── QuickJSContext.java │ ├── QuickJSException.java │ ├── QuickJSFunction.java │ └── QuickJSObject.java └── wrapper-js ├── extend └── libraries │ ├── console.js │ └── date-polyfill.js ├── src ├── index.mjs └── utils │ └── max-node-fs.mjs └── test └── console-test.js /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '**' 7 | 8 | jobs: 9 | publish: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | with: 17 | submodules: recursive 18 | 19 | - name: Install JDK 20 | uses: actions/setup-java@v3 21 | with: 22 | distribution: 'temurin' 23 | java-version: 17 24 | 25 | - name: Get release notes 26 | run: | 27 | echo "RELEASE_NOTES<> $GITHUB_ENV 28 | echo "$(awk '/^## ${{ github.ref_name }}/{flag=1;next}/^## /{flag=0}flag' CHANGELOG.md)" >> $GITHUB_ENV 29 | echo "EOF" >> $GITHUB_ENV 30 | 31 | - name: Set version for tag 32 | run: | 33 | echo "ORG_GRADLE_PROJECT_VERSION_NAME=${{ github.ref_name }}" >> $GITHUB_ENV 34 | 35 | - uses: gradle/gradle-build-action@v2 36 | 37 | - name: Publish 38 | run: ./gradlew publish 39 | env: 40 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} 41 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} 42 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_PRIVATE_KEY }} 43 | ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }} 44 | 45 | - name: Create Release 46 | uses: softprops/action-gh-release@v1 47 | with: 48 | token: ${{ github.token }} 49 | body: ${{ env.RELEASE_NOTES }} 50 | if: ${{ env.RELEASE_NOTES != '' }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.aar 4 | *.ap_ 5 | *.aab 6 | 7 | # Files for the ART/Dalvik VM 8 | *.dex 9 | 10 | # Java class files 11 | *.class 12 | 13 | # Generated files 14 | bin/ 15 | gen/ 16 | out/ 17 | # Uncomment the following line in case you need and you don't have the release build type files in your app 18 | # release/ 19 | 20 | # Gradle files 21 | .gradle/ 22 | build/ 23 | 24 | # Local configuration file (sdk path, etc) 25 | local.properties 26 | 27 | # Proguard folder generated by Eclipse 28 | proguard/ 29 | 30 | # Log Files 31 | *.log 32 | 33 | # Android Studio Navigation editor temp files 34 | .navigation/ 35 | 36 | # Android Studio captures folder 37 | captures/ 38 | 39 | # IntelliJ 40 | *.iml 41 | .idea/workspace.xml 42 | .idea/tasks.xml 43 | .idea/gradle.xml 44 | .idea/assetWizardSettings.xml 45 | .idea/dictionaries 46 | .idea/libraries 47 | # Android Studio 3 in .gitignore file. 48 | .idea/caches 49 | .idea/modules.xml 50 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 51 | .idea/navEditor.xml 52 | .idea/* 53 | 54 | # Keystore files 55 | # Uncomment the following lines if you do not want to check your keystore files in. 56 | #*.jks 57 | #*.keystore 58 | 59 | # External native build folder generated in Android Studio 2.2 and later 60 | .externalNativeBuild 61 | .cxx/ 62 | 63 | # Google Services (e.g. APIs or Firebase) 64 | # google-services.json 65 | 66 | # Freeline 67 | freeline.py 68 | freeline/ 69 | freeline_project_description.json 70 | 71 | # fastlane 72 | fastlane/report.xml 73 | fastlane/Preview.html 74 | fastlane/screenshots 75 | fastlane/test_output 76 | fastlane/readme.md 77 | 78 | # Version control 79 | vcs.xml 80 | 81 | # lint 82 | lint/intermediates/ 83 | lint/generated/ 84 | lint/outputs/ 85 | lint/tmp/ 86 | # lint/reports/ -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "native/quickjs"] 2 | path = native/quickjs 3 | url = https://github.com/HarlonWang/quickjs.git 4 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/sourcery-ai/sourcery 3 | rev: v1.23.0 4 | hooks: 5 | - id: sourcery 6 | # The best way to use Sourcery in a pre-commit hook: 7 | # * review only changed lines: 8 | # * omit the summary 9 | args: [--diff=git diff HEAD, --no-summary] 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 3.2.0 *(2025-05-15)* 4 | - feature: support 16 KB page sizes. 5 | 6 | ## 3.1.0 *(2025-03-17)* 7 | - Align internal and external version numbers to 3.1.0, no functional changes – codebase remains identical to 2.4.5. 8 | 9 | ## 2.4.5 *(2025-03-15)* 10 | - fix: not correctly released when an exception occurs during function execution 11 | 12 | ## 2.4.4 *(2025-02-12)* 13 | - 优化 toMap 的循环引用处理逻辑 14 | - quickjs 增加判空处理,解决 OOM 场景里的异常崩溃问题 15 | 16 | ## 2.4.3 *(2025-01-21)* 17 | - 添加了在将 JSObjects 转换为 Java Maps 时支持自定义映射创建。 18 | - 添加了 setGCThreshold 方法,用于控制垃圾回收阈值。 19 | 20 | ## 2.4.2 *(2025-01-10)* 21 | - 修复:源码执行模式下的字符串泄漏问题 22 | 23 | ## 2.4.1 *(2024-12-04)* 24 | - 修复:异步函数执行导致的崩溃问题 25 | - 优化:objectRecords 中的引用泄漏问题 26 | 27 | ## 2.4.0 *(2024-11-14)* 28 | - 新增方法: 获取使用内存的大小信息(getMemoryUsedSize) 29 | - 支持 ArrayBuffer 转为 Byte 数组(深拷贝,对性能有一些影响) 30 | 31 | ## 2.2.1 *(2024-09-29)* 32 | - JSObject 增加 toMap 方法,支持转 HashMap 类型 33 | 34 | ## 2.1.0 *(2024-09-20)* 35 | - 升级 QuickJS 至 2024-0214 版本 36 | - 优化不同平台的 string 转换 37 | 38 | ## 2.0.0 *(2024-08-01)* 39 | > :warning: 请注意,这是一次比较大的改动,如果是从老版本升级到该版本,需要你自己验证并回归自己的场景! 40 | - JSObject 相关类接口化改造,方便扩展 41 | - 稳定性提升 42 | - 修复了一些引用计数带来的崩溃异常 43 | - 优化了函数执行状态判断,避免一些复杂场景下的时机问题 44 | - 去处部分主动释放引用的逻辑,交由使用方自行释放 45 | - 其他可能会影响稳定的逻辑优化 46 | - 增加一些内存泄漏的排查能力 47 | - 一些历史 bugfix 48 | 49 | ## 1.0.0 *(2023-09-28)* 50 | - 修复:模块重复加载问题 51 | - 特性:支持模块字节码编译 52 | - 优化:console 日志模块 53 | - 优化:异常检测逻辑 54 | 55 | ## 0.21.1 *(2023-08-01)* 56 | - 修复:字节码执行中没有调用 `executePendingJobLoop` 57 | 58 | ## 0.21.0 *(2023-07-28)* 59 | - 优化:DumpObjects 日志输出到指定文件中方便查看 60 | - 优化:JSObject.toString 和 JavaScript 保持一致 61 | - 文档:修证书写错误 62 | 63 | ## 0.20.2 *(2023-06-30)* 64 | - 修复: cmake 构建提示 floor 方法的头文件缺失问题 65 | 66 | ## 0.20.1 *(2023-06-14)* 67 | - 特性: 优化 Long 和 Number、 BigInt 类型的互转逻辑 68 | 69 | ## 0.20.0 *(2023-06-07)* 70 | - 修复: JSString 对象没有释放引起的泄漏问题 71 | - 优化: 移除 js 层对 Array.at 的支持,由 c 层实现 72 | 73 | ## 0.19.3 *(2023-06-07)* 74 | 75 | - 修复:JNI 层 hashCode 无法获取问题 76 | 77 | ## 0.19.2 *(2023-06-06)* 78 | 79 | - 特性:支持 `Long` 类型数据在 JavaScript 中的传递 80 | 81 | ## 0.19.1 *(2023-06-05)* 82 | 83 | - 修复 `Attempt to remove non-JNI local reference` 的错误警告 84 | - 修复 `QuickJSWrapper` 析构函数执行时可能会出现的 `use deleted global reference` 异常 85 | - 重构 `JSCallFunction` 的绑定逻辑,避免使用 NewGlobalRef 带来的 global reference table overflow (max=51200) 异常 86 | - 集成 GitHub Action 提升版本发布效率 -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(QUICKJS_PATH "../../../native/quickjs") 2 | 3 | file(STRINGS "${QUICKJS_PATH}/VERSION" CONFIG_VERSION) 4 | 5 | add_definitions(-DCONFIG_VERSION=\"${CONFIG_VERSION}\") 6 | add_definitions(-DCONFIG_BIGNUM) 7 | 8 | file(GLOB wrapper_src 9 | "../../../native/cpp/*.cpp" 10 | "../../../native/cpp/*.h" 11 | ) 12 | 13 | file(GLOB quickjs_src 14 | "${QUICKJS_PATH}/cutils.c" 15 | "${QUICKJS_PATH}/cutils.h" 16 | "${QUICKJS_PATH}/libbf.c" 17 | "${QUICKJS_PATH}/libbf.h" 18 | "${QUICKJS_PATH}/libregexp-opcode.h" 19 | "${QUICKJS_PATH}/libregexp.c" 20 | "${QUICKJS_PATH}/libregexp.h" 21 | "${QUICKJS_PATH}/libunicode-table.h" 22 | "${QUICKJS_PATH}/libunicode.c" 23 | "${QUICKJS_PATH}/libunicode.h" 24 | "${QUICKJS_PATH}/list.h" 25 | "${QUICKJS_PATH}/quickjs-atom.h" 26 | "${QUICKJS_PATH}/quickjs-opcode.h" 27 | "${QUICKJS_PATH}/quickjs.c" 28 | "${QUICKJS_PATH}/quickjs.h" 29 | ) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QuickJS For Android/JVM 2 | QuickJS wrapper for Android/JVM. 3 | 4 | ## Feature 5 | - Java types are supported with JavaScript 6 | - Support promise execute 7 | - JavaScript exception handler 8 | - Compile bytecode 9 | - Supports converting JS object types to Java HashMap. 10 | - ESModule (import, export) 11 | 12 | Experimental Features Stability not guaranteed. 13 | - Supports ArrayBuffer to a byte array type. 14 | 15 | ## Download 16 | 17 | [![Maven Central](https://img.shields.io/maven-central/v/wang.harlon.quickjs/wrapper-android.svg?label=Maven%20Central&color=blue)](https://search.maven.org/search?q=g:%22wang.harlon.quickjs%22%20AND%20a:%22wrapper-android%22) 18 | 19 | ```Groovy 20 | repositories { 21 | mavenCentral() 22 | } 23 | 24 | dependencies { 25 | // Pick one: 26 | 27 | // 1. Android - Use wrapper in your public API: 28 | api 'wang.harlon.quickjs:wrapper-android:latest.version' 29 | 30 | // 2. JVM - Use wrapper in your implementation only: 31 | implementation 'wang.harlon.quickjs:wrapper-java:latest.version' 32 | } 33 | ``` 34 | 35 | ### SNAPSHOT 36 | [![Wrapper](https://img.shields.io/static/v1?label=snapshot&message=wrapper&logo=apache%20maven&color=yellowgreen)](https://s01.oss.sonatype.org/content/repositories/snapshots/wang/harlon/quickjs/wrapper-android/)
37 | 38 |
39 | See how to import the snapshot 40 | 41 | #### Including the SNAPSHOT 42 | Snapshots of the current development version of Wrapper are available, which track [the latest versions](https://s01.oss.sonatype.org/content/repositories/snapshots/wang/harlon/quickjs/wrapper-android/). 43 | 44 | To import snapshot versions on your project, add the code snippet below on your gradle file: 45 | ```Gradle 46 | repositories { 47 | maven { url 'https://s01.oss.sonatype.org/content/repositories/snapshots/' } 48 | } 49 | ``` 50 | 51 | Next, add the dependency below to your **module**'s `build.gradle` file: 52 | ```gradle 53 | dependencies { 54 | // For Android 55 | implementation "wang.harlon.quickjs:wrapper-android:latest-SNAPSHOT" 56 | // For JVM 57 | implementation "wang.harlon.quickjs:wrapper-java:latest-SNAPSHOT" 58 | } 59 | ``` 60 | 61 |
62 | 63 | ## Building the Project 64 | This repository use git submodules and so when you are checking out the app, you'll need to ensure the submodules are initialized properly. You can use the `--recursive` flag when cloning the project to do this. 65 | ```git 66 | git clone --recursive https://github.com/HarlonWang/quickjs-wrapper.git 67 | ``` 68 | 69 | Alternatively, if you already have the project checked out, you can initialize the submodules manually. 70 | ```git 71 | git submodule update --init 72 | ``` 73 | 74 | ## Usage 75 | 76 | ### Initialization 77 | In Android Platforms: 78 | ```Java 79 | // You usually need to initialize it before using it.. 80 | QuickJSLoader.init(); 81 | ``` 82 | 83 | [Refer to here for other platforms.](./wrapper-java/README.md) 84 | 85 | ### Create QuickJSContext 86 | 87 | ```Java 88 | QuickJSContext context = QuickJSContext.create(); 89 | 90 | // evaluating JavaScript 91 | context.evaluate("var a = 1 + 2;"); 92 | 93 | // destroy QuickJSContext 94 | context.destroy(); 95 | ``` 96 | 97 | ### Console Support 98 | ```Java 99 | context.setConsole(your console implementation.); 100 | ``` 101 | 102 | ### Supported Types 103 | 104 | #### Java and JavaScript can directly convert to each other for the following basic types 105 | | JavaScript | Java | 106 | |-------------|-------------------| 107 | | null | null | 108 | | undefined | null | 109 | | boolean | Boolean | 110 | | Number | Long/Int/Double | 111 | | string | String | 112 | | Array | JSArray | 113 | | object | JSObject | 114 | | Function | JSFunction | 115 | | ArrayBuffer | byte[](Deep copy) | 116 | 117 | Since JavaScript doesn't have a `long` type, additional information about `long`: 118 | 119 | Java --> JavaScript 120 | - The Long value <= Number.MAX_SAFE_INTEGER, will be convert to Number type. 121 | - The Long value > Number.MAX_SAFE_INTEGER, will be convert to BigInt type. 122 | - Number.MIN_SAFE_INTEGER is the same to above. 123 | 124 | JavaScript --> Java 125 | - Number(Int64) or BigInt --> Long type 126 | 127 | 128 | ### Set Property 129 | Java 130 | 131 | ```java 132 | QuickJSContext context = QuickJSContext.create(); 133 | JSObject globalObj = context.getGlobalObject(); 134 | JSObject repository = context.createNewJSObject(); 135 | obj1.setProperty("name", "QuickJS Wrapper"); 136 | obj1.setProperty("created", 2022); 137 | obj1.setProperty("version", 1.1); 138 | obj1.setProperty("signing_enabled", true); 139 | obj1.setProperty("getUrl", (JSCallFunction) args -> { 140 | return "https://github.com/HarlonWang/quickjs-wrapper"; 141 | }); 142 | globalObj.setProperty("repository", repository); 143 | repository.release(); 144 | ``` 145 | 146 | JavaScript 147 | 148 | ```javascript 149 | repository.name; // QuickJS Wrapper 150 | repository.created; // 2022 151 | repository.version; // 1.1 152 | repository.signing_enabled; // true 153 | repository.getUrl(); // https://github.com/HarlonWang/quickjs-wrapper 154 | ``` 155 | 156 | ### Get Property 157 | JavaScript 158 | 159 | ```JavaScript 160 | var repository = { 161 | name: 'QuickJS Wrapper', 162 | created: 2022, 163 | version: 1.1, 164 | signing_enabled: true, 165 | getUrl: (name) => { return 'https://github.com/HarlonWang/quickjs-wrapper'; } 166 | } 167 | ``` 168 | Java 169 | 170 | ```Java 171 | QuickJSContext context = QuickJSContext.create(); 172 | JSObject globalObject = context.getGlobalObject(); 173 | JSObject repository = globalObject.getJSObject("repository"); 174 | repository.getString("name"); // QuickJS Wrapper 175 | repository.getInteger("created"); // 2022 176 | repository.getDouble("version"); // 1.1 177 | repository.getBoolean("signing_enabled"); // true 178 | JSFunction fn = repository.getJSFunction("getUrl"); 179 | String url = fn.call(); // https://github.com/HarlonWang/quickjs-wrapper 180 | fn.release(); 181 | repository.release(); 182 | ``` 183 | 184 | ### Create JSObject in Java 185 | ```Java 186 | QuickJSContext context = QuickJSContext.create(); 187 | JSObject obj = context.createNewJSObject(); 188 | // When not in use, it needs to be released, otherwise it will cause a memory leak. 189 | obj.release(); 190 | ``` 191 | 192 | ### Create JSArray in Java 193 | ```Java 194 | QuickJSContext context = QuickJSContext.create(); 195 | JSArray array = context.createNewJSArray(); 196 | array.release(); 197 | ``` 198 | 199 | ### How to return Function to JavaScript in Java 200 | ```Java 201 | QuickJSContext context = createContext(); 202 | context.getGlobalObject().setProperty("test", args -> (JSCallFunction) args1 -> "123"); 203 | context.evaluate("console.log(test()());"); 204 | ``` 205 | 206 | Also, you can view it in `QuickJSTest.testReturnJSCallback` code 207 | 208 | 209 | ### Compile ByteCode 210 | 211 | ```Java 212 | byte[] code = context.compile("'hello, world!'.toUpperCase();"); 213 | context.execute(code); 214 | ``` 215 | 216 | ### ESModule 217 | Java 218 | ```Java 219 | // 1. string code mode 220 | context.setModuleLoader(new QuickJSContext.DefaultModuleLoader() { 221 | @Override 222 | public String getModuleStringCode(String moduleName) { 223 | if (moduleName.equals("a.js")) { 224 | return "export var name = 'Jack';\n" + 225 | "export var age = 18;"; 226 | } 227 | return null; 228 | } 229 | }); 230 | 231 | // 2. bytecode mode 232 | context.setModuleLoader(new QuickJSContext.BytecodeModuleLoader() { 233 | @Override 234 | public byte[] getModuleBytecode(String moduleName) { 235 | return context.compileModule("export var name = 'Jack';export var age = 18;", moduleName); 236 | } 237 | }); 238 | 239 | // 3. use `evaluateModule` for module script 240 | context.evaluateModule(...); 241 | ``` 242 | JavaScript 243 | ```JavaScript 244 | import {name, age} from './a.js'; 245 | 246 | console.log('name:' + name); // Jack 247 | console.log('age:' + age); // 18 248 | ``` 249 | 250 | ### Object release 251 | We typically recommend releasing reference relationships actively after using Java objects to avoid memory leaks. Additionally, the engine will release unreleased objects when destroy, but this timing may be a bit later. 252 | ```java 253 | JSFunction func = xxx.getJSFunction("test"); 254 | func.call(); 255 | func.release(); 256 | 257 | JSObject obj = xxx.getJSObject("test"); 258 | int a = obj.getString("123"); 259 | obj.release(); 260 | 261 | // If the return value is an object, it also needs to be released, 262 | JSObject ret = jsFunction.call(); 263 | ret.release(); 264 | 265 | // If you don't need to handle the return value, it is recommended to call the following method. 266 | jsFunction.callVoid(xxx); 267 | ``` 268 | 269 | It's important to note that if the result is being returned for use in JavaScript, there is no need to release it. 270 | ```java 271 | context.getGlobalObject().setProperty("test", new JSCallFunction() { 272 | @Override 273 | public Object call(Object... args) { 274 | JSObject ret = context.createNewJSObject(); 275 | // There is no need to call the release method here. 276 | // ret.release(); 277 | return ret; 278 | } 279 | }); 280 | ``` 281 | 282 | ## R8 / ProGuard 283 | If you are using R8 the shrinking and obfuscation rules are included automatically. 284 | 285 | ProGuard users must manually add the options from [consumer-rules.pro](/wrapper-android/consumer-rules.pro). 286 | 287 | ## Concurrency 288 | JavaScript runtimes are single threaded. All execution in the JavaScript runtime is guaranteed thread safe, by way of Java synchronization. 289 | 290 | ## Find this repository useful? 291 | Support it by joining __[stargazers](https://github.com/HarlonWang/quickjs-wrapper/stargazers)__ for this repository.
292 | Also, Sponsoring me will make this library even better! 293 | 294 | 295 | 296 | ## Stargazers over time 297 | 298 | [![Stargazers over time](https://starchart.cc/HarlonWang/quickjs-wrapper.svg)](https://starchart.cc/HarlonWang/quickjs-wrapper) 299 | 300 | ## Reference 301 | 302 | - [quickjs-java](https://github.com/cashapp/quickjs-java) 303 | - [quack](https://github.com/koush/quack) 304 | - [quickjs-android](https://github.com/taoweiji/quickjs-android) 305 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | } 4 | 5 | android { 6 | namespace 'com.whl.quickjs.wrapper.sample' 7 | compileSdk 35 8 | 9 | defaultConfig { 10 | applicationId "com.whl.quickjs.wrapper.sample" 11 | minSdkVersion 21 12 | targetSdk 35 13 | versionCode 1 14 | versionName "1.0" 15 | 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled true 22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | 26 | sourceSets { 27 | androidTest.root '../wrapper-android/src/androidTest' 28 | androidTest.java.srcDirs '../wrapper-android/src/androidTest/java' 29 | androidTest.assets.srcDirs '../wrapper-android/src/androidTest/assets' 30 | } 31 | 32 | // packagingOptions { 33 | // doNotStrip "*/*/*.so" 34 | // } 35 | } 36 | 37 | dependencies { 38 | 39 | implementation 'androidx.appcompat:appcompat:1.7.0' 40 | implementation 'com.google.android.material:material:1.12.0' 41 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4' 42 | 43 | implementation (project(':wrapper-android')) 44 | 45 | testImplementation 'junit:junit:4.14-SNAPSHOT' 46 | androidTestImplementation 'androidx.test.ext:junit:1.2.1' 47 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' 48 | androidTestImplementation project(path: ':wrapper-java') 49 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/whl/quickjs/wrapper/sample/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.whl.quickjs.wrapper.sample; 2 | 3 | import androidx.appcompat.app.AppCompatActivity; 4 | 5 | import android.os.Bundle; 6 | import android.widget.TextView; 7 | 8 | import com.whl.quickjs.android.QuickJSLoader; 9 | import com.whl.quickjs.wrapper.QuickJSContext; 10 | 11 | public class MainActivity extends AppCompatActivity { 12 | 13 | QuickJSContext jsContext; 14 | 15 | @Override 16 | protected void onCreate(Bundle savedInstanceState) { 17 | super.onCreate(savedInstanceState); 18 | setContentView(R.layout.activity_main); 19 | 20 | QuickJSLoader.init(); 21 | 22 | jsContext = QuickJSContext.create(); 23 | jsContext.evaluate("var text = 'Hello QuickJS';"); 24 | String text = jsContext.getGlobalObject().getString("text"); 25 | TextView textView = findViewById(R.id.text); 26 | textView.setText(text); 27 | } 28 | 29 | @Override 30 | protected void onDestroy() { 31 | super.onDestroy(); 32 | jsContext.destroy(); 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarlonWang/quickjs-wrapper/8b36a1c352f7e8ea2b29fe98d790435ed35a6f1b/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarlonWang/quickjs-wrapper/8b36a1c352f7e8ea2b29fe98d790435ed35a6f1b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarlonWang/quickjs-wrapper/8b36a1c352f7e8ea2b29fe98d790435ed35a6f1b/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarlonWang/quickjs-wrapper/8b36a1c352f7e8ea2b29fe98d790435ed35a6f1b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarlonWang/quickjs-wrapper/8b36a1c352f7e8ea2b29fe98d790435ed35a6f1b/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarlonWang/quickjs-wrapper/8b36a1c352f7e8ea2b29fe98d790435ed35a6f1b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarlonWang/quickjs-wrapper/8b36a1c352f7e8ea2b29fe98d790435ed35a6f1b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarlonWang/quickjs-wrapper/8b36a1c352f7e8ea2b29fe98d790435ed35a6f1b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarlonWang/quickjs-wrapper/8b36a1c352f7e8ea2b29fe98d790435ed35a6f1b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarlonWang/quickjs-wrapper/8b36a1c352f7e8ea2b29fe98d790435ed35a6f1b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | quickjs-android-wrapper 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/src/test/java/com/whl/quickjs/wrapper/sample/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.whl.quickjs.wrapper.sample; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | import com.vanniktech.maven.publish.SonatypeHost 2 | 3 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 4 | buildscript { 5 | repositories { 6 | google() 7 | mavenCentral() 8 | } 9 | dependencies { 10 | classpath "com.android.tools.build:gradle:8.5.1" 11 | classpath 'com.vanniktech:gradle-maven-publish-plugin:0.22.0' 12 | // NOTE: Do not place your application dependencies here; they belong 13 | // in the individual module build.gradle files 14 | } 15 | } 16 | 17 | allprojects { 18 | repositories { 19 | google() 20 | mavenCentral() 21 | } 22 | } 23 | 24 | subprojects { 25 | if (project.name != "app") { 26 | 27 | apply plugin: "com.vanniktech.maven.publish" 28 | 29 | group = "wang.harlon.quickjs" 30 | 31 | mavenPublishing { 32 | publishToMavenCentral(SonatypeHost.S01, true) 33 | 34 | signAllPublications() 35 | 36 | pom { 37 | name = "QuickJS Wrapper" 38 | description = "Quickjs wrapper library for Android/JVM." 39 | inceptionYear = "2022" 40 | url = "https://github.com/HarlonWang/quickjs-wrapper" 41 | licenses { 42 | license { 43 | name = "The Apache License, Version 2.0" 44 | url = "http://www.apache.org/licenses/LICENSE-2.0.txt" 45 | distribution = "http://www.apache.org/licenses/LICENSE-2.0.txt" 46 | } 47 | } 48 | developers { 49 | developer { 50 | id = "HarlonWang" 51 | name = "Harlon Wang" 52 | url = "https://github.com/HarlonWang" 53 | } 54 | } 55 | scm { 56 | url = "https://github.com/HarlonWang/quickjs-wrapper" 57 | connection = "scm:git:git://github.com/HarlonWang/quickjs-wrapper.git" 58 | developerConnection = "scm:git:ssh://github.com/HarlonWang/quickjs-wrapper.git" 59 | } 60 | } 61 | } 62 | } 63 | } 64 | 65 | task clean(type: Delete) { 66 | delete rootProject.buildDir 67 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarlonWang/quickjs-wrapper/8b36a1c352f7e8ea2b29fe98d790435ed35a6f1b/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Sep 23 10:18:31 CST 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /images/alipay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarlonWang/quickjs-wrapper/8b36a1c352f7e8ea2b29fe98d790435ed35a6f1b/images/alipay.jpg -------------------------------------------------------------------------------- /images/wechat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarlonWang/quickjs-wrapper/8b36a1c352f7e8ea2b29fe98d790435ed35a6f1b/images/wechat.png -------------------------------------------------------------------------------- /native/cpp/quickjs_context_jni.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "quickjs_wrapper.h" 4 | #include 5 | 6 | extern "C" 7 | JNIEXPORT void JNICALL 8 | Java_com_whl_quickjs_wrapper_QuickJSContext_destroyContext(JNIEnv *env, jobject thiz, 9 | jlong context) { 10 | delete reinterpret_cast(context); 11 | } 12 | 13 | extern "C" 14 | JNIEXPORT jobject JNICALL 15 | Java_com_whl_quickjs_wrapper_QuickJSContext_evaluate(JNIEnv *env, jobject thiz, jlong context, jstring script, 16 | jstring file_name) { 17 | if (script == nullptr) { 18 | env->ThrowNew(env->FindClass("java/lang/NullPointerException"), "Script cannot be null"); 19 | return nullptr; 20 | } 21 | 22 | if (file_name == nullptr) { 23 | env->ThrowNew(env->FindClass("java/lang/NullPointerException"), "File name cannot be null"); 24 | return nullptr; 25 | } 26 | 27 | auto wrapper = reinterpret_cast(context); 28 | return wrapper->evaluate(env, thiz, script, file_name); 29 | } 30 | 31 | extern "C" 32 | JNIEXPORT jobject JNICALL 33 | Java_com_whl_quickjs_wrapper_QuickJSContext_getGlobalObject(JNIEnv *env, jobject thiz, 34 | jlong context) { 35 | auto wrapper = reinterpret_cast(context); 36 | return wrapper->getGlobalObject(env, thiz); 37 | } 38 | 39 | extern "C" 40 | JNIEXPORT jobject JNICALL 41 | Java_com_whl_quickjs_wrapper_QuickJSContext_getProperty(JNIEnv *env, jobject thiz, jlong context, jlong value, 42 | jstring name) { 43 | if (name == nullptr) { 44 | env->ThrowNew(env->FindClass("java/lang/NullPointerException"), "Property Name cannot be null"); 45 | return nullptr; 46 | } 47 | 48 | auto wrapper = reinterpret_cast(context); 49 | return wrapper->getProperty(env, thiz, value, name); 50 | } 51 | 52 | extern "C" 53 | JNIEXPORT jobject JNICALL 54 | Java_com_whl_quickjs_wrapper_QuickJSContext_call(JNIEnv *env, jobject thiz, jlong context, 55 | jlong func, jlong this_obj, jint this_obj_tag, jobjectArray args) { 56 | auto wrapper = reinterpret_cast(context); 57 | return wrapper->call(env, thiz, func, this_obj, this_obj_tag, args); 58 | } 59 | 60 | extern "C" 61 | JNIEXPORT jstring JNICALL 62 | Java_com_whl_quickjs_wrapper_QuickJSContext_stringify(JNIEnv *env, jobject thiz, jlong context, 63 | jlong value) { 64 | auto wrapper = reinterpret_cast(context); 65 | return wrapper->jsonStringify(env, value); 66 | }extern "C" 67 | JNIEXPORT jint JNICALL 68 | Java_com_whl_quickjs_wrapper_QuickJSContext_length(JNIEnv *env, jobject thiz, jlong context, 69 | jlong value) { 70 | auto wrapper = reinterpret_cast(context); 71 | return wrapper->length(env, value); 72 | }extern "C" 73 | JNIEXPORT jobject JNICALL 74 | Java_com_whl_quickjs_wrapper_QuickJSContext_get(JNIEnv *env, jobject thiz, jlong context, jlong value, 75 | jint index) { 76 | auto wrapper = reinterpret_cast(context); 77 | return wrapper->get(env, thiz, value, index); 78 | }extern "C" 79 | JNIEXPORT jlong JNICALL 80 | Java_com_whl_quickjs_wrapper_QuickJSContext_createContext(JNIEnv *env, jobject thiz, jlong runtime) { 81 | auto *wrapper = new(std::nothrow) QuickJSWrapper(env, thiz, reinterpret_cast(runtime)); 82 | if (!wrapper || !wrapper->context || !wrapper->runtime) { 83 | delete wrapper; 84 | wrapper = nullptr; 85 | } 86 | 87 | return reinterpret_cast(wrapper); 88 | }extern "C" 89 | JNIEXPORT void JNICALL 90 | Java_com_whl_quickjs_wrapper_QuickJSContext_setProperty(JNIEnv *env, jobject thiz, jlong context, 91 | jlong this_obj, jstring name, 92 | jobject value) { 93 | if (name == nullptr) { 94 | env->ThrowNew(env->FindClass("java/lang/NullPointerException"), "Property Name cannot be null"); 95 | return; 96 | } 97 | 98 | auto wrapper = reinterpret_cast(context); 99 | wrapper->setProperty(env, thiz, this_obj, name, value); 100 | }extern "C" 101 | JNIEXPORT void JNICALL 102 | Java_com_whl_quickjs_wrapper_QuickJSContext_freeValue(JNIEnv *env, jobject thiz, jlong context, 103 | jlong value) { 104 | auto wrapper = reinterpret_cast(context); 105 | wrapper->freeValue(value); 106 | }extern "C" 107 | JNIEXPORT void JNICALL 108 | Java_com_whl_quickjs_wrapper_QuickJSContext_dupValue(JNIEnv *env, jobject thiz, jlong context, 109 | jlong value) { 110 | auto wrapper = reinterpret_cast(context); 111 | wrapper->dupValue(value); 112 | }extern "C" 113 | JNIEXPORT void JNICALL 114 | Java_com_whl_quickjs_wrapper_QuickJSContext_freeDupValue(JNIEnv *env, jobject thiz, jlong context, 115 | jlong value) { 116 | auto wrapper = reinterpret_cast(context); 117 | wrapper->freeDupValue(value); 118 | }extern "C" 119 | JNIEXPORT jobject JNICALL 120 | Java_com_whl_quickjs_wrapper_QuickJSContext_parseJSON(JNIEnv *env, jobject thiz, jlong context, 121 | jstring json) { 122 | if (json == nullptr) { 123 | env->ThrowNew(env->FindClass("java/lang/NullPointerException"), "JSON cannot be null"); 124 | return nullptr; 125 | } 126 | 127 | auto wrapper = reinterpret_cast(context); 128 | return wrapper->parseJSON(env, thiz, json); 129 | }extern "C" 130 | JNIEXPORT jbyteArray JNICALL 131 | Java_com_whl_quickjs_wrapper_QuickJSContext_compile(JNIEnv *env, jobject thiz, jlong context, 132 | jstring source_code, jstring file_name, jboolean isModule) { 133 | if (source_code == nullptr) { 134 | env->ThrowNew(env->FindClass("java/lang/NullPointerException"), "Source code cannot be null"); 135 | return nullptr; 136 | } 137 | 138 | if (file_name == nullptr) { 139 | env->ThrowNew(env->FindClass("java/lang/NullPointerException"), "File name cannot be null"); 140 | return nullptr; 141 | } 142 | 143 | auto wrapper = reinterpret_cast(context); 144 | return wrapper->compile(env, source_code, file_name, isModule); 145 | }extern "C" 146 | JNIEXPORT jobject JNICALL 147 | Java_com_whl_quickjs_wrapper_QuickJSContext_execute(JNIEnv *env, jobject thiz, jlong context, 148 | jbyteArray bytecode) { 149 | auto wrapper = reinterpret_cast(context); 150 | return wrapper->execute(env, thiz, bytecode); 151 | }extern "C" 152 | JNIEXPORT jobject JNICALL 153 | Java_com_whl_quickjs_wrapper_QuickJSContext_evaluateModule(JNIEnv *env, jobject thiz, jlong context, 154 | jstring script, jstring file_name) { 155 | if (script == nullptr) { 156 | env->ThrowNew(env->FindClass("java/lang/NullPointerException"), "Script cannot be null"); 157 | return nullptr; 158 | } 159 | 160 | if (file_name == nullptr) { 161 | env->ThrowNew(env->FindClass("java/lang/NullPointerException"), "File name cannot be null"); 162 | return nullptr; 163 | } 164 | 165 | auto wrapper = reinterpret_cast(context); 166 | return wrapper->evaluateModule(env, thiz, script, file_name); 167 | }extern "C" 168 | JNIEXPORT void JNICALL 169 | Java_com_whl_quickjs_wrapper_QuickJSContext_set(JNIEnv *env, jobject thiz, jlong context, 170 | jlong this_obj, jobject value, jint index) { 171 | auto wrapper = reinterpret_cast(context); 172 | wrapper->set(env, thiz, this_obj, value, index); 173 | } 174 | extern "C" 175 | JNIEXPORT void JNICALL 176 | Java_com_whl_quickjs_wrapper_QuickJSContext_setMaxStackSize(JNIEnv *env, jclass thiz, 177 | jlong runtime, jint size) { 178 | auto *rt = reinterpret_cast(runtime); 179 | JS_SetMaxStackSize(rt, size); 180 | } 181 | extern "C" 182 | JNIEXPORT jboolean JNICALL 183 | Java_com_whl_quickjs_wrapper_QuickJSContext_isLiveObject(JNIEnv *env, jclass thiz, jlong runtime, 184 | jlong value) { 185 | auto *rt = reinterpret_cast(runtime); 186 | JSValue jsObj = JS_MKPTR(JS_TAG_OBJECT, reinterpret_cast(value)); 187 | if (JS_IsLiveObject(rt, jsObj)) { 188 | return JNI_TRUE; 189 | } 190 | 191 | return JNI_FALSE; 192 | } 193 | extern "C" 194 | JNIEXPORT void JNICALL 195 | Java_com_whl_quickjs_wrapper_QuickJSContext_runGC(JNIEnv *env, jclass thiz, jlong runtime) { 196 | auto *rt = reinterpret_cast(runtime); 197 | JS_RunGC(rt); 198 | } 199 | extern "C" 200 | JNIEXPORT jlong JNICALL 201 | Java_com_whl_quickjs_wrapper_QuickJSContext_createRuntime(JNIEnv *env, jclass clazz) { 202 | auto *rt = JS_NewRuntime(); 203 | return reinterpret_cast(rt); 204 | } 205 | extern "C" 206 | JNIEXPORT void JNICALL 207 | Java_com_whl_quickjs_wrapper_QuickJSContext_setMemoryLimit(JNIEnv *env, jclass clazz, jlong runtime, 208 | jint size) { 209 | auto *rt = reinterpret_cast(runtime); 210 | JS_SetMemoryLimit(rt, size); 211 | } 212 | extern "C" 213 | JNIEXPORT void JNICALL 214 | Java_com_whl_quickjs_wrapper_QuickJSContext_dumpMemoryUsage(JNIEnv *env, jclass clazz, 215 | jlong runtime, jstring file_name) { 216 | auto *rt = reinterpret_cast(runtime); 217 | 218 | if (file_name == nullptr) { 219 | JSMemoryUsage stats; 220 | JS_ComputeMemoryUsage(rt, &stats); 221 | JS_DumpMemoryUsage(stdout, &stats, rt); 222 | } else { 223 | const char *path = env->GetStringUTFChars(file_name, JNI_FALSE); 224 | auto file = fopen(path, "w"); 225 | env->ReleaseStringUTFChars(file_name, path); 226 | if (!file) { 227 | env->ThrowNew(env->FindClass("java/lang/NullPointerException"), "File cannot be null"); 228 | return; 229 | } 230 | 231 | JSMemoryUsage stats; 232 | JS_ComputeMemoryUsage(rt, &stats); 233 | JS_DumpMemoryUsage(file, &stats, rt); 234 | 235 | fclose(file); 236 | } 237 | } 238 | extern "C" 239 | JNIEXPORT void JNICALL 240 | Java_com_whl_quickjs_wrapper_QuickJSContext_dumpObjects(JNIEnv *env, jobject thiz, jlong runtime, 241 | jstring file_name) { 242 | auto *rt = reinterpret_cast(runtime); 243 | 244 | if (file_name == nullptr) { 245 | JS_DumpObjects(rt); 246 | } else { 247 | const char *path = env->GetStringUTFChars(file_name, JNI_FALSE); 248 | // 这里重定向打印日志到指定文件,方便查看。 249 | // todo 打印完需要再恢复到控制台打印,参考:https://cloud.tencent.com/developer/article/1544633 250 | auto file = freopen(path, "w", stdout); 251 | env->ReleaseStringUTFChars(file_name, path); 252 | if (!file) { 253 | env->ThrowNew(env->FindClass("java/lang/NullPointerException"), "File cannot be null"); 254 | return; 255 | } 256 | 257 | JSMemoryUsage stats; 258 | JS_ComputeMemoryUsage(rt, &stats); 259 | JS_DumpMemoryUsage(stdout, &stats, rt); 260 | 261 | JS_DumpObjects(rt); 262 | 263 | fclose(file); 264 | } 265 | 266 | } 267 | extern "C" 268 | JNIEXPORT jobject JNICALL 269 | Java_com_whl_quickjs_wrapper_QuickJSContext_getOwnPropertyNames(JNIEnv *env, jobject thiz, 270 | jlong context, jlong obj_value) { 271 | auto wrapper = reinterpret_cast(context); 272 | return wrapper->getOwnPropertyNames(env, thiz, obj_value); 273 | } 274 | extern "C" 275 | JNIEXPORT jlong JNICALL 276 | Java_com_whl_quickjs_wrapper_QuickJSContext_getMemoryUsedSize(JNIEnv *env, jobject thiz, 277 | jlong runtime) { 278 | auto *rt = reinterpret_cast(runtime); 279 | JSMemoryUsage usage; 280 | JS_ComputeMemoryUsage(rt, &usage); 281 | return (jlong)usage.memory_used_size; 282 | } 283 | extern "C" 284 | JNIEXPORT void JNICALL 285 | Java_com_whl_quickjs_wrapper_QuickJSContext_setGCThreshold(JNIEnv *env, jobject thiz, jlong runtime, 286 | jint size) { 287 | auto *rt = reinterpret_cast(runtime); 288 | // use -1 to disable automatic GC 289 | if (size < 0) { 290 | size = -1; 291 | } 292 | JS_SetGCThreshold(rt, size); 293 | } -------------------------------------------------------------------------------- /native/cpp/quickjs_extend_libraries.h: -------------------------------------------------------------------------------- 1 | #ifndef QUICKJS_EXTEND_LIBRARIES 2 | #define QUICKJS_EXTEND_LIBRARIES 3 | 4 | #include 5 | #include 6 | #include "../quickjs/quickjs.h" 7 | 8 | const char *DATE_POLYFILL = R"lit((() => { 9 | const _Date = Date; 10 | // use _Date avoid recursion in _parse. 11 | const _parse = (date) => { 12 | if (date === null) { 13 | // null is invalid 14 | return new _Date(NaN); 15 | } 16 | if (date === undefined) { 17 | // today 18 | return new _Date(); 19 | } 20 | if (date instanceof Date) { 21 | return new _Date(date); 22 | } 23 | 24 | if (typeof date === 'string' && !/Z$/i.test(date)) { 25 | // YYYY-MM-DD HH:mm:ss.sssZ 26 | const d = date.match(/^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[Tt\s]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?[.:]?(\d+)?$/); 27 | if (d) { 28 | let YYYY = d[1]; 29 | let MM = d[2] - 1 || 0; 30 | let DD = d[3] || 1; 31 | 32 | const HH = d[4] || 0; 33 | const mm = d[5] || 0; 34 | const ss = d[6] || 0; 35 | const sssZ = (d[7] || '0').substring(0, 3); 36 | 37 | // Consider that only date strings (such as "1970-01-01") will be processed as UTC instead of local time. 38 | let utc = (d[4] === undefined) && (d[5] === undefined) && (d[6] === undefined) && (d[7] === undefined); 39 | if (utc) { 40 | return new Date(Date.UTC(YYYY, MM, DD, HH, mm, ss, sssZ)); 41 | } 42 | return new Date(YYYY, MM, DD, HH, mm, ss, sssZ); 43 | } 44 | } 45 | 46 | // everything else 47 | return new _Date(date); 48 | }; 49 | 50 | const handler = { 51 | construct: function (target, args) { 52 | if (args.length === 1 && typeof args[0] === 'string') { 53 | return _parse(args[0]); 54 | } 55 | 56 | return new target(...args); 57 | }, 58 | get(target, prop) { 59 | if (typeof target[prop] === 'function' && target[prop].name === 'parse') { 60 | return new Proxy(target[prop], { 61 | apply: (target, thisArg, argumentsList) => { 62 | if (argumentsList.length === 1 && typeof argumentsList[0] === 'string') { 63 | return _parse(argumentsList[0]).getTime(); 64 | } 65 | 66 | return Reflect.apply(target, thisArg, argumentsList); 67 | } 68 | }); 69 | } else { 70 | return Reflect.get(target, prop); 71 | } 72 | } 73 | }; 74 | 75 | Date = new Proxy(Date, handler); 76 | })();)lit"; 77 | 78 | const char *CONSOLE = R"lit(// Init format at first. 79 | { 80 | const LINE = "\n" 81 | const TAB = " " 82 | const SPACE = " " 83 | 84 | function format(value, opt) { 85 | const defaultOpt = { 86 | maxStringLength: 10000, 87 | depth: 2, 88 | maxArrayLength: 100, 89 | seen: [], 90 | reduceStringLength: 100 91 | } 92 | if (!opt) { 93 | opt = defaultOpt 94 | } else { 95 | opt = Object.assign(defaultOpt, opt) 96 | } 97 | 98 | return formatValue(value, opt, 0) 99 | } 100 | 101 | function formatValue(value, opt, recurseTimes) { 102 | if (typeof value !== 'object' && typeof value !== 'function') { 103 | return formatPrimitive(value, opt) 104 | } 105 | 106 | if (value === null) { 107 | return 'null' 108 | } 109 | 110 | if (typeof value === 'function') { 111 | return formatFunction(value) 112 | } 113 | 114 | if (typeof value === 'object') { 115 | if (opt.seen.includes(value)) { 116 | let index = 1 117 | if (opt.circular === undefined) { 118 | opt.circular = new Map() 119 | opt.circular.set(value, index) 120 | } else { 121 | index = opt.circular.get(value) 122 | if (index === undefined) { 123 | index = opt.circular.size + 1 124 | opt.circular.set(value, index) 125 | } 126 | } 127 | 128 | return `[Circular *${index}]` 129 | } 130 | 131 | if (opt.depth !== null && ((recurseTimes - 1) === opt.depth)) { 132 | if (value instanceof Array) { 133 | return '[Array]' 134 | } 135 | return '[Object]' 136 | } 137 | 138 | recurseTimes++ 139 | opt.seen.push(value) 140 | const string = formatObject(value, opt, recurseTimes) 141 | opt.seen.pop() 142 | return string 143 | } 144 | } 145 | 146 | function formatObject(value, opt, recurseTimes) { 147 | if (value instanceof RegExp) { 148 | return `${value.toString()}` 149 | } 150 | 151 | if (value instanceof Error) { 152 | return `${value.toString()}` 153 | } 154 | 155 | if (value instanceof Promise) { 156 | // quickjs 环境下通过 native 提供的方式获取 Promise 状态 157 | if (typeof getPromiseState !== "undefined") { 158 | const { result, state} = getPromiseState(value) 159 | if (state === 'fulfilled') { 160 | return `Promise { ${formatValue(result, opt, recurseTimes)} }` 161 | } else if (state === 'rejected'){ 162 | return `Promise { ${formatValue(result, opt, recurseTimes)} }` 163 | } else if (state === 'pending'){ 164 | return `Promise { }` 165 | } 166 | } else { 167 | return `Promise {${formatValue(value, opt, recurseTimes)}}` 168 | } 169 | } 170 | 171 | if (value instanceof Array) { 172 | return formatArray(value, opt, recurseTimes) 173 | } 174 | 175 | if (value instanceof Float64Array) { 176 | return `Float64Array(1) [ ${value} ]` 177 | } 178 | 179 | if (value instanceof BigInt64Array) { 180 | return `BigInt64Array(1) [ ${value}n ]` 181 | } 182 | 183 | if (value instanceof Map) { 184 | return formatMap(value, opt, recurseTimes) 185 | } 186 | 187 | return formatProperty(value, opt, recurseTimes) 188 | } 189 | 190 | function formatProperty(value, opt, recurseTimes) { 191 | let string = '' 192 | string += '{' 193 | const keys = Object.keys(value) 194 | const length = keys.length 195 | for (let i = 0; i < length; i++) { 196 | if (i === 0) { 197 | string += SPACE 198 | } 199 | string += LINE 200 | string += TAB.repeat(recurseTimes) 201 | 202 | const key = keys[i] 203 | string += `${key}: ` 204 | string += formatValue(value[key], opt, recurseTimes) 205 | if (i < length -1) { 206 | string += ',' 207 | } 208 | string += SPACE 209 | } 210 | 211 | string += LINE 212 | string += TAB.repeat(recurseTimes - 1) 213 | string += '}' 214 | 215 | if (string.length < opt.reduceStringLength) { 216 | string = string.replaceAll(LINE, "").replaceAll(TAB, "") 217 | } 218 | 219 | return string 220 | } 221 | 222 | function formatMap(value, opt, recurseTimes) { 223 | let string = `Map(${value.size}) ` 224 | string += '{' 225 | let isEmpty = true 226 | value.forEach((v, k, map) => { 227 | isEmpty = false 228 | string += ` ${format(k, opt, recurseTimes)} => ${format(v, opt, recurseTimes)}` 229 | string += ',' 230 | }) 231 | 232 | if (!isEmpty) { 233 | // 删除最后多余的逗号 234 | string = string.substr(0, string.length -1) + ' ' 235 | } 236 | 237 | string += '}' 238 | return string 239 | } 240 | 241 | function formatArray(value, opt, recurseTimes) { 242 | let string = '[' 243 | value.forEach((item, index, array) => { 244 | if (index === 0) { 245 | string += ' ' 246 | } 247 | string += formatValue(item, opt, recurseTimes) 248 | if (index === opt.maxArrayLength - 1) { 249 | string += `... ${array.length - opt.maxArrayLength} more item${array.length - opt.maxArrayLength > 1 ? 's' : ''}` 250 | } else if (index !== array.length - 1) { 251 | string += ',' 252 | } 253 | string += ' ' 254 | }) 255 | string += ']' 256 | return string 257 | } 258 | 259 | function formatFunction(value) { 260 | let type = 'Function' 261 | 262 | if (value.constructor.name === 'AsyncFunction') { 263 | type = 'AsyncFunction' 264 | } 265 | 266 | if (value.constructor.name === 'GeneratorFunction') { 267 | type = 'GeneratorFunction' 268 | } 269 | 270 | if (value.constructor.name === 'AsyncGeneratorFunction') { 271 | type = 'AsyncGeneratorFunction' 272 | } 273 | 274 | let fn = `${value.name ? `: ${value.name}` : ' (anonymous)'}` 275 | return `[${type + fn}]` 276 | } 277 | 278 | function formatPrimitive(value, opt) { 279 | const type = typeof value 280 | switch (type) { 281 | case "string": 282 | return formatString(value, opt) 283 | case "number": 284 | return Object.is(value, -0) ? '-0' : `${value}` 285 | case "bigint": 286 | return `${String(value)}n` 287 | case "boolean": 288 | return `${value}` 289 | case "undefined": 290 | return "undefined" 291 | case "symbol": 292 | return `${value.toString()}` 293 | default: 294 | return value.toString 295 | } 296 | } 297 | 298 | function formatString(value, opt) { 299 | let trailer = '' 300 | if (opt.maxStringLength && value.length > opt.maxStringLength) { 301 | const remaining = value.length - opt.maxStringLength 302 | value = value.slice(0, opt.maxStringLength) 303 | trailer = `... ${remaining} more character${remaining > 1 ? 's' : ''}` 304 | } 305 | 306 | return `'${value}'${trailer}` 307 | } 308 | 309 | globalThis.format = format 310 | } 311 | 312 | // Then console init. 313 | { 314 | globalThis.console = { 315 | stdout: function (level, msg) { 316 | throw new Error("When invoke console stuff, you should be set a stdout of platform to console.stdout.") 317 | }, 318 | log: function (...args) { 319 | this.print("log", ...args) 320 | }, 321 | debug: function(...args) { 322 | this.print("debug", ...args) 323 | }, 324 | info: function (...args) { 325 | this.print("info", ...args) 326 | }, 327 | warn: function (...args) { 328 | this.print("warn", ...args) 329 | }, 330 | error: function (...args) { 331 | this.print("error", ...args) 332 | }, 333 | print: function (level, ...args) { 334 | let msg = '' 335 | args.forEach((value, index) => { 336 | if (index > 0) { 337 | msg += ", " 338 | } 339 | 340 | msg += globalThis.format(value) 341 | }) 342 | 343 | this.stdout(level, msg) 344 | } 345 | } 346 | })lit"; 347 | 348 | 349 | static inline void loadExtendLibraries(JSContext *ctx) { 350 | JS_FreeValue(ctx, JS_Eval(ctx, DATE_POLYFILL, strlen(DATE_POLYFILL), "date-polyfill.js", JS_EVAL_TYPE_GLOBAL)); 351 | JS_FreeValue(ctx, JS_Eval(ctx, CONSOLE, strlen(CONSOLE), "console.js", JS_EVAL_TYPE_GLOBAL)); 352 | } 353 | 354 | #endif //QUICKJS_EXTEND_LIBRARIES -------------------------------------------------------------------------------- /native/cpp/quickjs_wrapper.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by yonglan.whl on 2021/7/14. 3 | // 4 | 5 | #ifndef QUICKJS_TEST_CONTEXT_WRAPPER_H 6 | #define QUICKJS_TEST_CONTEXT_WRAPPER_H 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | using namespace std; 13 | 14 | #include "../quickjs/quickjs.h" 15 | #include 16 | #include 17 | 18 | class QuickJSWrapper { 19 | private: 20 | jstring toJavaString(JNIEnv *env, JSValue value) const; 21 | jobject toJavaObject(JNIEnv *env, jobject thiz, JSValueConst this_obj, JSValueConst value) const; 22 | JSValue toJSValue(JNIEnv *env, jobject thiz, jobject value) const; 23 | 24 | public: 25 | JNIEnv *jniEnv; 26 | jobject jniThiz; 27 | JSRuntime *runtime; 28 | JSContext *context; 29 | 30 | queue unhandledRejections; 31 | 32 | jclass objectClass; 33 | jclass booleanClass; 34 | jclass integerClass; 35 | jclass longClass; 36 | jclass doubleClass; 37 | jclass stringClass; 38 | jclass jsObjectClass; 39 | jclass jsArrayClass; 40 | jclass jsFunctionClass; 41 | jclass jsCallFunctionClass; 42 | jclass quickjsContextClass; 43 | jclass moduleLoaderClass; 44 | jclass creatorClass; 45 | jclass byteArrayClass; 46 | JSValue ownPropertyNames; 47 | 48 | jmethodID booleanValueOf; 49 | jmethodID integerValueOf; 50 | jmethodID longValueOf; 51 | jmethodID doubleValueOf; 52 | 53 | jmethodID booleanGetValue; 54 | jmethodID integerGetValue; 55 | jmethodID longGetValue; 56 | jmethodID doubleGetValue; 57 | jmethodID jsObjectGetValue; 58 | 59 | jmethodID callFunctionBackM; 60 | jmethodID removeCallFunctionM; 61 | jmethodID callFunctionHashCodeM; 62 | jmethodID creatorM; 63 | jmethodID newObjectM; 64 | jmethodID newArrayM; 65 | jmethodID newFunctionM; 66 | 67 | QuickJSWrapper(JNIEnv *env, jobject thiz, JSRuntime *rt); 68 | ~QuickJSWrapper(); 69 | 70 | jobject evaluate(JNIEnv*, jobject thiz, jstring script, jstring file_name); 71 | jobject getGlobalObject(JNIEnv*, jobject thiz) const; 72 | jobject getProperty(JNIEnv*, jobject thiz, jlong value, jstring name); 73 | void setProperty(JNIEnv*, jobject thiz, jlong this_obj, jstring name, jobject value) const; 74 | jobject call(JNIEnv *env, jobject thiz, jlong func, jlong this_obj, jint this_obj_tag, jobjectArray args); 75 | jstring jsonStringify(JNIEnv *env, jlong value) const; 76 | jint length(JNIEnv *env, jlong value) const; 77 | jobject get(JNIEnv *env, jobject thiz, jlong value, jint index); 78 | void set(JNIEnv *env, jobject thiz, jlong this_obj, jobject value, jint index); 79 | JSValue jsFuncCall(int callback_id, JSValueConst this_val, int argc, JSValueConst *argv); 80 | void removeCallFunction(int callback_id) const; 81 | void freeValue(jlong) const; 82 | void dupValue(jlong) const; 83 | void freeDupValue(jlong) const; 84 | jobject parseJSON(JNIEnv*, jobject, jstring); 85 | 86 | // JS --> bytecode 87 | jbyteArray compile(JNIEnv*, jstring, jstring, jboolean) const; 88 | // bytecode --> result 89 | jobject execute(JNIEnv*, jobject, jbyteArray); 90 | 91 | jobject evaluateModule(JNIEnv *env, jobject thiz, jstring script, jstring file_name); 92 | 93 | jobject getOwnPropertyNames(JNIEnv *env, jobject thiz, jlong obj); 94 | }; 95 | 96 | #endif //QUICKJS_TEST_CONTEXT_WRAPPER_H 97 | -------------------------------------------------------------------------------- /remarks.md: -------------------------------------------------------------------------------- 1 | # Other 2 | ## 一些说明: 3 | - getProperty 多次获取同一个 JSObject,需要每次 free 下,保持计数平衡 4 | - 作为参数的 JSObject 不可以 FreeValue 释放,会报错,猜测是参数的 JSObject 会自动释放,不需要额外 FreeValue 5 | 6 | ## 日志调试 7 | 在 `quickjs.c` 文件加入以下代码,可以在 `Android` 层打印 `logcat` 日志,方便排查泄漏问题 8 | 9 | // 注意需要放在 #include 后面,不然会编译报错 10 | #include 11 | #define printf(...) __android_log_print(ANDROID_LOG_DEBUG, "__quickjs__", __VA_ARGS__); 12 | 13 | 14 | #define DUMP_LEAKS 1 15 | 16 | ## 常见错误: 17 | - fault addr 0x18 in tid 22363:一般是 JSValue 已经被 FreeValue,再次调用就会报这个错误 18 | 19 | - list_empty(&rt->gc_obj_list)" failed:一般是使用的 JSValue 没有被 FreeValue 导致,可以打开 `quickjs.c` 里的 `DUMP_LEAKS` 开关查看没有释放的泄漏对象 20 | 21 | - TypeError: not a function" failed:一般是该 function 对象被释放后,再次调用的时候就会报这个错 22 | 23 | 24 | context.setProperty(context.getGlobalObject(), "setTimeout", new JSCallFunction() { 25 | @Override 26 | public Object call(Object... args) { 27 | JSFunction argFunc = (JSFunction) args[0]; 28 | int delay = (int) args[1]; 29 | 30 | // 1. 这里调用没问题,因为还没有执行到 return 31 | // context.call(argFunc, context.getGlobalObject()); 32 | 33 | new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { 34 | @Override 35 | public void run() { 36 | // 3. 这里调用有问题,因为是延迟执行,argFunc 实际已经被回收了 37 | // 再调用就会报 TypeError: not a function" failed 38 | context.call(argFunc, context.getGlobalObject()); 39 | } 40 | }, delay); 41 | 42 | // 2. 这里 return 执行完就会对 argFunc 回收处理. 43 | return null; 44 | } 45 | }); 46 | 47 | - globalObject 的引用计数默认是 2,暂时未弄清楚具体逻辑,目前的释放逻辑是每次 getGlobalObject 后都会 Free 下。待后续优化 48 | 49 | - String/Atom 内存泄漏问题:虽然调用了 JSCString 的释放方法,但是因为其所在的对象是 GlobalObject,无法被释放,导致一直会被持有,这里需要进一步优化. 50 | 51 | - `void gc_decref_child(JSRuntime *, JSGCObjectHeader *): assertion "p->ref_count > 0" failed` 52 | - 原因:某个对象引用计数为未加一,导致减一的时候校验失败 53 | - 排查方式:先打印出 `gc_decref_child` 里 `p->ref_count <= 0` 的对象信息: 54 | ```c 55 | static void gc_decref_child(JSRuntime *rt, JSGCObjectHeader *p) 56 | { 57 | // 打印异常对象信息 58 | if (p->ref_count <= 0) { 59 | JS_DumpGCObject(rt, p) 60 | } 61 | assert(p->ref_count > 0); 62 | ... 63 | } 64 | ``` 65 | 因为执行到方法时,对象一般已经释放,会显示为 `null`,但是可以看到指针信息,可以选择以下任一方式定位到具体对象信息: 66 | - 方式1:打开 `quickjs.c` 里的 DUMP_FREE 开关,打印出所有的 `free` 对象,然后指针去匹配查找到释放前的对象信息 67 | - 方式2:找到 `__JS_FreeValueRT` 方法,并注释以下代码,不释放对象,这样在 `gc_decref_child` 里就可以看到对象的具体信息。 68 | 69 | 70 | case JS_TAG_OBJECT: 71 | case JS_TAG_FUNCTION_BYTECODE: 72 | { 73 | // JSGCObjectHeader *p = JS_VALUE_GET_PTR(v); 74 | // if (rt->gc_phase != JS_GC_PHASE_REMOVE_CYCLES) { 75 | // list_del(&p->link); 76 | // list_add(&p->link, &rt->gc_zero_ref_count_list); 77 | // if (rt->gc_phase == JS_GC_PHASE_NONE) { 78 | // free_zero_refcount(rt); 79 | // } 80 | // } -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = "quickjs-wrapper" 2 | include ':app' 3 | include ':wrapper-android' 4 | include ':wrapper-java' 5 | -------------------------------------------------------------------------------- /wrapper-android/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /wrapper-android/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | } 4 | 5 | android { 6 | namespace 'com.whl.quickjs.android' 7 | compileSdk 35 8 | 9 | defaultConfig { 10 | minSdkVersion 21 11 | targetSdk 35 12 | versionCode 1 13 | versionName "1.0" 14 | 15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 16 | consumerProguardFiles "consumer-rules.pro" 17 | 18 | externalNativeBuild { 19 | cmake { 20 | cppFlags '-DIS_ANDROID=TRUE' 21 | } 22 | } 23 | } 24 | 25 | buildTypes { 26 | release { 27 | minifyEnabled true 28 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'consumer-rules.pro', 'proguard-rules.pro' 29 | } 30 | } 31 | 32 | externalNativeBuild { 33 | cmake { 34 | path file('src/main/CMakeLists.txt') 35 | } 36 | } 37 | 38 | // packagingOptions { 39 | // doNotStrip "*/*/*.so" 40 | // } 41 | 42 | } 43 | 44 | dependencies { 45 | api api(project(':wrapper-java',)) 46 | testImplementation 'junit:junit:4.14-SNAPSHOT' 47 | androidTestImplementation 'androidx.test.ext:junit:1.2.1' 48 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' 49 | } -------------------------------------------------------------------------------- /wrapper-android/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -keepclasseswithmembers class * { 2 | native ; 3 | } 4 | -keep class com.whl.quickjs.**{*;} -------------------------------------------------------------------------------- /wrapper-android/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /wrapper-android/src/androidTest/java/com/whl/quickjs/wrapper/QuickJSCompileTest.java: -------------------------------------------------------------------------------- 1 | package com.whl.quickjs.wrapper; 2 | 3 | import org.junit.Before; 4 | import org.junit.Rule; 5 | import org.junit.Test; 6 | import org.junit.rules.ExpectedException; 7 | 8 | import static org.junit.Assert.*; 9 | 10 | import com.whl.quickjs.android.QuickJSLoader; 11 | 12 | public class QuickJSCompileTest { 13 | 14 | @Rule 15 | public ExpectedException thrown = ExpectedException.none(); 16 | 17 | @Before 18 | public void setup() { 19 | QuickJSLoader.init(); 20 | } 21 | 22 | @Test 23 | public void helloWorld() { 24 | try (QuickJSContext context = QuickJSContext.create()) { 25 | byte[] code = context.compile("'hello, world!'.toUpperCase();"); 26 | Object hello = context.execute(code); 27 | assertEquals(hello, "HELLO, WORLD!"); 28 | } 29 | } 30 | 31 | @Test 32 | public void testDifferentContexts() { 33 | byte[] code; 34 | try (QuickJSContext context = QuickJSContext.create()) { 35 | code = context.compile("'hello, world!'.toUpperCase();"); 36 | } 37 | 38 | try (QuickJSContext context = QuickJSContext.create()) { 39 | Object hello = context.execute(code); 40 | assertEquals(hello, "HELLO, WORLD!"); 41 | } 42 | } 43 | 44 | @Test 45 | public void testPromise() { 46 | try (QuickJSContext context = QuickJSContext.create()) { 47 | byte[] bytes = context.compile("var ret; new Promise((resolve, reject) => { ret = 'resolved'; }); ret;"); 48 | Object ret = context.execute(bytes); 49 | assertEquals(ret, "resolved"); 50 | } 51 | } 52 | 53 | @Test(expected = QuickJSException.class) 54 | public void testThrowErrorWithFileName() { 55 | try (QuickJSContext context = QuickJSContext.create()) { 56 | byte[] bytes = context.compile("test;", "test.js"); 57 | context.execute(bytes); 58 | } 59 | } 60 | 61 | @Test 62 | public void testFreeValueReturnedOfExecute() { 63 | QuickJSLoader.startRedirectingStdoutStderr("quickjs_android"); 64 | try (QuickJSContext context = QuickJSContext.create()) { 65 | QuickJSLoader.initConsoleLog(context); 66 | 67 | byte[] bytes = context.compile("test = () => { console.log('test'); }"); 68 | JSObject ret = (JSObject) context.execute(bytes); 69 | ret.release(); 70 | } 71 | } 72 | 73 | @Test 74 | public void testCompileModule() { 75 | try (QuickJSContext context = QuickJSContext.create()) { 76 | QuickJSLoader.initConsoleLog(context); 77 | context.setModuleLoader(new QuickJSContext.BytecodeModuleLoader() { 78 | @Override 79 | public byte[] getModuleBytecode(String moduleName) { 80 | return context.compileModule("export const a = {name: 'test'};", moduleName); 81 | } 82 | }); 83 | byte[] bytes = context.compileModule("import {a} from 'a.js'; if(a.name !== 'test') { throw new Error('failed') }", "aaa.js"); 84 | context.execute(bytes); 85 | } 86 | } 87 | 88 | @Test 89 | public void testCompileModuleWithoutModuleLoader() { 90 | thrown.expect(QuickJSException.class); 91 | thrown.expectMessage("Failed to load module, the ModuleLoader can not be null!"); 92 | 93 | try (QuickJSContext context = QuickJSContext.create()) { 94 | context.compileModule("import { a } from 'a.js';"); 95 | } 96 | } 97 | 98 | @Test 99 | public void testCompileModuleWithMockModuleLoader() { 100 | thrown.expect(QuickJSException.class); 101 | thrown.expectMessage("Could not find export 'a' in module 'a.js'"); 102 | 103 | try (QuickJSContext context = QuickJSContext.create()) { 104 | context.setModuleLoader(new QuickJSContext.DefaultModuleLoader() { 105 | @Override 106 | public String getModuleStringCode(String moduleName) { 107 | return ""; 108 | } 109 | }); 110 | // 在 ModuleLoader 中返回空字符串,可以实现仅编译当前模块字节码,而不用编译它所依赖的模块 111 | byte[] bytes = context.compileModule("import { a } from 'a.js';"); 112 | context.execute(bytes); 113 | } 114 | } 115 | 116 | @Test 117 | public void testStringCodeModuleLoaderReturnNull() { 118 | thrown.expect(QuickJSException.class); 119 | thrown.expectMessage("Failed to load module, cause string code was null!"); 120 | 121 | try (QuickJSContext context = QuickJSContext.create()) { 122 | context.setModuleLoader(new QuickJSContext.DefaultModuleLoader() { 123 | @Override 124 | public String getModuleStringCode(String moduleName) { 125 | return null; 126 | } 127 | }); 128 | context.compileModule("import { a } from 'a.js';"); 129 | } 130 | } 131 | 132 | @Test 133 | public void testBytecodeModuleLoaderReturnNull() { 134 | thrown.expect(QuickJSException.class); 135 | thrown.expectMessage("Failed to load module, cause bytecode was null!"); 136 | 137 | try (QuickJSContext context = QuickJSContext.create()) { 138 | context.setModuleLoader(new QuickJSContext.BytecodeModuleLoader() { 139 | @Override 140 | public byte[] getModuleBytecode(String moduleName) { 141 | return null; 142 | } 143 | }); 144 | context.compileModule("import { a } from 'a.js';"); 145 | } 146 | } 147 | 148 | } 149 | -------------------------------------------------------------------------------- /wrapper-android/src/androidTest/java/com/whl/quickjs/wrapper/QuickJSFreeValueTest.java: -------------------------------------------------------------------------------- 1 | package com.whl.quickjs.wrapper; 2 | 3 | import android.util.Log; 4 | import org.junit.After; 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | 8 | import static org.junit.Assert.*; 9 | 10 | import com.whl.quickjs.android.QuickJSLoader; 11 | 12 | public class QuickJSFreeValueTest { 13 | 14 | private QuickJSContext context; 15 | 16 | @Before 17 | public void setup() { 18 | QuickJSLoader.init(); 19 | QuickJSLoader.startRedirectingStdoutStderr("quickjs"); 20 | context = QuickJSContext.create(); 21 | } 22 | 23 | @After 24 | public void teardown() { 25 | context.destroy(); 26 | } 27 | 28 | @Test 29 | public void globalFreeTest() { 30 | JSObject globalObj = context.getGlobalObject(); 31 | JSObject globalObj1 = context.getGlobalObject(); 32 | JSObject globalObj2 = context.getGlobalObject(); 33 | JSObject globalObj3 = context.getGlobalObject(); 34 | JSObject globalObj4 = context.getGlobalObject(); 35 | 36 | 37 | globalObj.setProperty("name", "Jack"); 38 | assertEquals("Jack", globalObj.getProperty("name")); 39 | 40 | 41 | 42 | globalObj1.setProperty("age", 12); 43 | 44 | assertEquals(12, globalObj4.getProperty("age")); 45 | } 46 | 47 | @Test 48 | public void evalTestFree() { 49 | JSObject evaluate = (JSObject) context.evaluate("function test() {\n" + 50 | "\treturn {name: \"hello\", age: 12, sex: \"男\"};\n" + 51 | "}\n" + 52 | "\n" + 53 | "test();"); 54 | 55 | String result = evaluate.stringify(); 56 | 57 | assertEquals("{\"name\":\"hello\",\"age\":12,\"sex\":\"男\"}", result); 58 | 59 | evaluate.release(); 60 | 61 | context.evaluate("var user = {};"); 62 | 63 | JSObject user = (JSObject) context.getGlobalObject().getProperty("user"); 64 | user.setProperty("name", "Jack"); 65 | user.release(); 66 | 67 | JSFunction function = (JSFunction) context.getGlobalObject().getProperty("test"); 68 | JSObject result1 = (JSObject) function.call(); 69 | JSObject result2 = (JSObject) function.call(); 70 | 71 | String name = (String) result1.getProperty("name"); 72 | assertEquals("hello", name); 73 | 74 | String name1 = (String) result1.getProperty("name"); 75 | assertEquals("hello", name1); 76 | 77 | result2.release(); 78 | result1.release(); 79 | function.release(); 80 | } 81 | 82 | @Test 83 | public void testState() { 84 | context.getGlobalObject().setProperty("setState", args -> { 85 | JSObject ret = (JSObject) args[0]; 86 | Log.d("test", ret.toString()); 87 | ret.release(); 88 | return "test"; 89 | }); 90 | 91 | context.evaluate("setState({age: 12});"); 92 | } 93 | 94 | @Test 95 | public void testGetProperty() { 96 | context.evaluate("var obj1 = {age: {age_a: 12}};"); 97 | 98 | JSObject obj1 = (JSObject) context.getGlobalObject().getProperty("obj1"); 99 | 100 | JSObject age = (JSObject) obj1.getProperty("age"); 101 | JSObject age1 = (JSObject) obj1.getProperty("age"); 102 | JSObject age2 = (JSObject) obj1.getProperty("age"); 103 | JSObject age3 = (JSObject) obj1.getProperty("age"); 104 | // 105 | age.release(); 106 | age1.release(); 107 | age2.release(); 108 | age3.release(); 109 | 110 | obj1.release(); 111 | } 112 | 113 | @Test 114 | public void funcArgsFreeTest() { 115 | // set console.log 116 | context.evaluate("var console = {};"); 117 | JSObject console = (JSObject) context.getGlobalObject().getProperty("console"); 118 | console.setProperty("log", new JSCallFunction() { 119 | @Override 120 | public Object call(Object... args) { 121 | StringBuilder b = new StringBuilder(); 122 | for (Object o: args) { 123 | b.append(o == null ? "null" : o.toString()); 124 | } 125 | 126 | Log.d("tiny-console", b.toString()); 127 | return null; 128 | } 129 | }); 130 | 131 | context.evaluate("var state = {};\n" + 132 | "\n" + 133 | "\n" + 134 | "function setState(data) {\n" + 135 | "\tstate = data;\n" + 136 | "}\n" + 137 | "\n" + 138 | "function getState() {\n" + 139 | "\treturn state;\n" + 140 | "}"); 141 | 142 | 143 | context.evaluate("\n" + 144 | "function stateTest() {\n" + 145 | "\tsetState({count: 0});\n" + 146 | "\n" + 147 | "\tconsole.log(getState().count);\n" + 148 | "}\n"); 149 | 150 | context.evaluate("stateTest();"); 151 | context.evaluate("setState({count: 1});"); 152 | context.evaluate("console.log(getState().count);"); 153 | console.release(); 154 | } 155 | 156 | } 157 | -------------------------------------------------------------------------------- /wrapper-android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /wrapper-android/src/main/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # For more information about using CMake with Android Studio, read the 2 | # documentation: https://d.android.com/studio/projects/add-native-code.html 3 | 4 | # Sets the minimum version of CMake required to build the native library. 5 | 6 | cmake_minimum_required(VERSION 3.10.2) 7 | 8 | project(quickjs-android-wrapper) 9 | 10 | include(${CMAKE_CURRENT_SOURCE_DIR}/../../../CMakeLists.txt) 11 | 12 | file(GLOB android_src 13 | "cpp/quickjs_android.cpp" 14 | ) 15 | 16 | add_library( # Sets the name of the library. 17 | quickjs-android-wrapper 18 | 19 | # Sets the library as a shared library. 20 | SHARED 21 | 22 | # Provides a relative path to your source file(s). 23 | ${wrapper_src} ${quickjs_src} ${android_src}) 24 | 25 | # Searches for a specified prebuilt library and stores the path as a 26 | # variable. Because CMake includes system libraries in the search path by 27 | # default, you only need to specify the name of the public NDK library 28 | # you want to add. CMake verifies that the library exists before 29 | # completing its build. 30 | 31 | find_library( # Sets the name of the path variable. 32 | log-lib 33 | 34 | # Specifies the name of the NDK library that 35 | # you want CMake to locate. 36 | log ) 37 | 38 | # Specifies libraries CMake should link to your target library. You 39 | # can link multiple libraries, such as libraries you define in this 40 | # build script, prebuilt third-party libraries, or system libraries. 41 | 42 | target_link_libraries( # Specifies the target library. 43 | quickjs-android-wrapper 44 | "-Wl,-z,max-page-size=16384" 45 | # Links the target library to the log library 46 | # included in the NDK. 47 | ${log-lib}) -------------------------------------------------------------------------------- /wrapper-android/src/main/assets/test_assert_define.js: -------------------------------------------------------------------------------- 1 | function fail(message) { 2 | if (message == null) { 3 | throw Error("❌assert error.") 4 | } 5 | 6 | throw Error(message); 7 | } 8 | 9 | export function assertEquals(expected, actual) { 10 | if (expected !== actual) { 11 | fail(`❌assert failed, expected:[${expected}] but was:[${actual}]`) 12 | } 13 | } 14 | 15 | export function assertTrue(condition) { 16 | if (!condition) { 17 | fail() 18 | } 19 | } 20 | 21 | export function assertFalse(condition) { 22 | if (condition) { 23 | fail() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /wrapper-android/src/main/assets/test_base_module1.mjs: -------------------------------------------------------------------------------- 1 | import {name, age} from './test_base_module2.mjs'; 2 | import {assertEquals} from "./test_assert_define.js"; 3 | 4 | assertEquals(name, "Jack") 5 | assertEquals(age, 18) 6 | -------------------------------------------------------------------------------- /wrapper-android/src/main/assets/test_base_module2.mjs: -------------------------------------------------------------------------------- 1 | export var name = 'Jack'; 2 | export var age = 18; 3 | -------------------------------------------------------------------------------- /wrapper-android/src/main/assets/test_module_import_dynamic.js: -------------------------------------------------------------------------------- 1 | import {assertEquals} from "./test_assert_define.js"; 2 | 3 | // dynamic import 4 | import("test_base_module2.mjs").then((res) => { 5 | assertEquals(res.name, "Jack") 6 | assertEquals(res.age, 18) 7 | }) 8 | -------------------------------------------------------------------------------- /wrapper-android/src/main/assets/test_polyfill_date.js: -------------------------------------------------------------------------------- 1 | function assertDate(expected, actual) { 2 | if ((Date.parse(expected) === new Date(expected).getTime()) && (Date.parse(expected) === actual)) { 3 | console.log('✅assert passed with ' + expected, 'Date.parse = ' + Date.parse(expected), 'Date.construct = ' + new Date(expected).getTime(), 'actual = ' + actual); 4 | } else { 5 | console.log('❌assert failed with ' + expected, 'Date.parse = ' + Date.parse(expected), 'Date.construct = ' + new Date(expected).getTime(), 'actual = ' + actual); 6 | throw Error('parse failed.'); 7 | } 8 | } 9 | 10 | assertDate('20130108', 1357603200000); 11 | assertDate('2018-04-24', 1524528000000); 12 | assertDate('2018-04-24 11:12', 1524539520000); 13 | assertDate('2018-05-02 11:12:13', 1525230733000); 14 | assertDate('2018-05-02 11:12:13.998', 1525230733998); 15 | assertDate('2018-4-1', 1522540800000); 16 | assertDate('2018-4-1 11:12', 1522552320000); 17 | assertDate('2018-4-1 1:1:1:223', 1522515661223); 18 | assertDate('2018-01', 1514764800000); 19 | assertDate('2018', 1514764800000); 20 | assertDate('2018-05-02T11:12:13Z', 1525259533000); 21 | -------------------------------------------------------------------------------- /wrapper-android/src/main/cpp/quickjs_android.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | // 5 | // Created by SM2254 on 2022/12/3. 6 | // 7 | #include 8 | #include 9 | #include 10 | 11 | // Start threads to redirect stdout and stderr to logcat. 12 | int pipe_stdout[2]; 13 | int pipe_stderr[2]; 14 | pthread_t thread_stdout; 15 | pthread_t thread_stderr; 16 | const char *QUICKJS_TAG = "quickjs_android"; 17 | 18 | void *thread_stderr_func(void*) { 19 | ssize_t redirect_size; 20 | char buf[2048]; 21 | while((redirect_size = read(pipe_stderr[0], buf, sizeof buf - 1)) > 0) { 22 | //__android_log will add a new line anyway. 23 | if(buf[redirect_size - 1] == '\n') 24 | --redirect_size; 25 | buf[redirect_size] = 0; 26 | __android_log_write(ANDROID_LOG_ERROR, QUICKJS_TAG, buf); 27 | } 28 | return 0; 29 | } 30 | 31 | void *thread_stdout_func(void*) { 32 | ssize_t redirect_size; 33 | char buf[2048]; 34 | while((redirect_size = read(pipe_stdout[0], buf, sizeof buf - 1)) > 0) { 35 | //__android_log will add a new line anyway. 36 | if(buf[redirect_size - 1] == '\n') 37 | --redirect_size; 38 | buf[redirect_size] = 0; 39 | __android_log_write(ANDROID_LOG_INFO, QUICKJS_TAG, buf); 40 | } 41 | return 0; 42 | } 43 | 44 | int start_redirecting_stdout_stderr() { 45 | //set stdout as unbuffered. 46 | setvbuf(stdout, 0, _IONBF, 0); 47 | pipe(pipe_stdout); 48 | dup2(pipe_stdout[1], STDOUT_FILENO); 49 | 50 | //set stderr as unbuffered. 51 | setvbuf(stderr, 0, _IONBF, 0); 52 | pipe(pipe_stderr); 53 | dup2(pipe_stderr[1], STDERR_FILENO); 54 | 55 | if(pthread_create(&thread_stdout, 0, thread_stdout_func, 0) == -1) 56 | return -1; 57 | pthread_detach(thread_stdout); 58 | 59 | if(pthread_create(&thread_stderr, 0, thread_stderr_func, 0) == -1) 60 | return -1; 61 | pthread_detach(thread_stderr); 62 | 63 | return 0; 64 | } 65 | 66 | extern "C" 67 | JNIEXPORT void JNICALL 68 | Java_com_whl_quickjs_android_QuickJSLoader_startRedirectingStdoutStderr(JNIEnv *env, jclass clazz, 69 | jstring tag) { 70 | if (tag != nullptr) { 71 | QUICKJS_TAG = env->GetStringUTFChars(tag, JNI_FALSE); 72 | } 73 | 74 | //Start threads to show stdout and stderr in logcat. 75 | if (start_redirecting_stdout_stderr() == -1) { 76 | __android_log_write(ANDROID_LOG_ERROR, QUICKJS_TAG, "Couldn't start redirecting stdout and stderr to logcat."); 77 | } 78 | 79 | printf("started redirecting stdout and stderr to logcat."); 80 | } -------------------------------------------------------------------------------- /wrapper-android/src/main/java/com/whl/quickjs/android/QuickJSLoader.java: -------------------------------------------------------------------------------- 1 | package com.whl.quickjs.android; 2 | 3 | import android.util.Log; 4 | 5 | import com.whl.quickjs.wrapper.QuickJSContext; 6 | 7 | /** 8 | * Created by Harlon Wang on 2022/8/12. 9 | */ 10 | public final class QuickJSLoader { 11 | 12 | public static void init() { 13 | System.loadLibrary("quickjs-android-wrapper"); 14 | } 15 | 16 | static final class LogcatConsole implements QuickJSContext.Console { 17 | 18 | private final String tag; 19 | 20 | public LogcatConsole(String tag) { 21 | this.tag = tag; 22 | } 23 | 24 | @Override 25 | public void log(String info) { 26 | Log.d(tag, info); 27 | } 28 | 29 | @Override 30 | public void info(String info) { 31 | Log.i(tag, info); 32 | } 33 | 34 | @Override 35 | public void warn(String info) { 36 | Log.w(tag, info); 37 | } 38 | 39 | @Override 40 | public void error(String info) { 41 | Log.e(tag, info); 42 | } 43 | } 44 | 45 | /** 46 | * See {@link QuickJSContext#setConsole(QuickJSContext.Console)} 47 | */ 48 | @Deprecated 49 | public static void initConsoleLog(QuickJSContext context) { 50 | initConsoleLog(context, new LogcatConsole("quickjs")); 51 | } 52 | 53 | /** 54 | * See {@link QuickJSContext#setConsole(QuickJSContext.Console)} 55 | */ 56 | @Deprecated 57 | public static void initConsoleLog(QuickJSContext context, String tag) { 58 | initConsoleLog(context, new LogcatConsole(tag)); 59 | } 60 | 61 | /** 62 | * See {@link QuickJSContext#setConsole(QuickJSContext.Console)} 63 | */ 64 | @Deprecated 65 | public static void initConsoleLog(QuickJSContext context, QuickJSContext.Console console) { 66 | context.setConsole(console); 67 | } 68 | 69 | /** 70 | * Start threads to show stdout and stderr in logcat. 71 | * @param tag Android Tag 72 | */ 73 | public native static void startRedirectingStdoutStderr(String tag); 74 | 75 | } 76 | -------------------------------------------------------------------------------- /wrapper-java/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /wrapper-java/README.md: -------------------------------------------------------------------------------- 1 | # 构建 2 | ## 环境要求: 3 | + JDK & JAVA_HOME 环境变量 4 | + 安装好 cmake ninja 5 | 6 | ### cmake 安装方式 mac 7 | ``` 8 | brew install cmake 9 | ``` 10 | 11 | ### ninja 安装方式 mac 12 | ``` 13 | brew install ninja 14 | ``` 15 | 16 | ### cmake 安装方式 linux 17 | 卸载系统自带的版本(太老) 18 | 19 | ``` 20 | apt remove cmake 21 | 22 | ``` 23 | 24 | ``` 25 | wget https://github.com/Kitware/CMake/releases/download/v3.24.0-rc5/cmake-3.24.0-rc5.tar.gz 26 | ``` 27 | 28 | ``` 29 | tar -xvf cmake-3.24.0-rc5.tar.gz 30 | ``` 31 | 32 | ``` 33 | cd make-3.24.0-rc5 34 | ``` 35 | 36 | ``` 37 | ./configure && make && make install 38 | ``` 39 | 40 | ### ninja 安装方式 linux 41 | ``` 42 | apt install ninja-build 43 | ``` 44 | 45 | ## 构建动态链接库 46 | 打开 `terminal` 窗口,执行以下命令: 47 | ```shell 48 | // 进入 wrapper-java 目录 49 | cd wrapper-java 50 | 51 | // step 1 52 | cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_MAKE_PROGRAM=ninja -G Ninja -S ./src/main -B ./build/cmake 53 | 54 | // step 2 55 | cmake --build ./build/cmake --target quickjs-java-wrapper -j 6 56 | ``` 57 | 58 | ## 产物 59 | so 链库地址: 60 | ```shell 61 | wrapper-java/build/cmake/libquickjs-java-wrapper.dylib 62 | ``` 63 | 64 | ## TODO 65 | - [ ] 跨平台编译方式(在单一平台编译出其他平台产物) -------------------------------------------------------------------------------- /wrapper-java/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java-library' 3 | } 4 | 5 | java { 6 | sourceCompatibility = JavaVersion.VERSION_1_8 7 | targetCompatibility = JavaVersion.VERSION_1_8 8 | } -------------------------------------------------------------------------------- /wrapper-java/src/main/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.23) 2 | project(quickjs-java-wrapper) 3 | 4 | set(CMAKE_CXX_STANDARD 11) 5 | 6 | include(${CMAKE_CURRENT_SOURCE_DIR}/../../../CMakeLists.txt) 7 | 8 | add_library( # Sets the name of the library. 9 | quickjs-java-wrapper 10 | 11 | # Sets the library as a shared library. 12 | SHARED 13 | 14 | # Provides a relative path to your source file(s). 15 | ${wrapper_src} ${quickjs_src}) 16 | 17 | 18 | include_directories($ENV{JAVA_HOME}/include/) 19 | include_directories($ENV{JAVA_HOME}/include/darwin) 20 | include_directories($ENV{JAVA_HOME}/include/linux) -------------------------------------------------------------------------------- /wrapper-java/src/main/java/com/whl/quickjs/wrapper/JSArray.java: -------------------------------------------------------------------------------- 1 | package com.whl.quickjs.wrapper; 2 | 3 | public interface JSArray extends JSObject { 4 | int length(); 5 | Object get(int index); 6 | void set(Object value, int index); 7 | } 8 | -------------------------------------------------------------------------------- /wrapper-java/src/main/java/com/whl/quickjs/wrapper/JSCallFunction.java: -------------------------------------------------------------------------------- 1 | package com.whl.quickjs.wrapper; 2 | 3 | public interface JSCallFunction { 4 | Object call(Object... args); 5 | } 6 | -------------------------------------------------------------------------------- /wrapper-java/src/main/java/com/whl/quickjs/wrapper/JSFunction.java: -------------------------------------------------------------------------------- 1 | package com.whl.quickjs.wrapper; 2 | 3 | public interface JSFunction extends JSObject { 4 | Object call(Object... args); 5 | void callVoid(Object... args); 6 | } 7 | -------------------------------------------------------------------------------- /wrapper-java/src/main/java/com/whl/quickjs/wrapper/JSMethod.java: -------------------------------------------------------------------------------- 1 | package com.whl.quickjs.wrapper; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Retention(value = RetentionPolicy.RUNTIME) 9 | @Target(value = {ElementType.METHOD}) 10 | public @interface JSMethod { 11 | } -------------------------------------------------------------------------------- /wrapper-java/src/main/java/com/whl/quickjs/wrapper/JSObject.java: -------------------------------------------------------------------------------- 1 | package com.whl.quickjs.wrapper; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | 7 | public interface JSObject { 8 | 9 | void setStackTrace(Throwable trace); 10 | Throwable getStackTrace(); 11 | 12 | void setProperty(String name, String value); 13 | void setProperty(String name, int value); 14 | void setProperty(String name, long value); 15 | void setProperty(String name, JSObject value); 16 | void setProperty(String name, boolean value); 17 | void setProperty(String name, double value); 18 | void setProperty(String name, byte[] value); 19 | void setProperty(String name, JSCallFunction value); 20 | void setProperty(String name, Class clazz); 21 | long getPointer(); 22 | QuickJSContext getContext(); 23 | Object getProperty(String name); 24 | @Deprecated 25 | String getStringProperty(String name); 26 | String getString(String name); 27 | @Deprecated 28 | Integer getIntProperty(String name); 29 | Integer getInteger(String name); 30 | @Deprecated 31 | Boolean getBooleanProperty(String name); 32 | Boolean getBoolean(String name); 33 | @Deprecated 34 | Double getDoubleProperty(String name); 35 | Double getDouble(String name); 36 | Long getLong(String name); 37 | byte[] getBytes(String name); 38 | @Deprecated 39 | JSObject getJSObjectProperty(String name); 40 | JSObject getJSObject(String name); 41 | @Deprecated 42 | JSFunction getJSFunctionProperty(String name); 43 | JSFunction getJSFunction(String name); 44 | @Deprecated 45 | JSArray getJSArrayProperty(String name); 46 | JSArray getJSArray(String name); 47 | @Deprecated 48 | JSArray getOwnPropertyNames(); 49 | JSArray getNames(); 50 | String stringify(); 51 | boolean isAlive(); 52 | void release(); 53 | void hold(); 54 | int getRefCount(); 55 | boolean isRefCountZero(); 56 | /** 57 | * 引用计数减一,目前仅将对象返回到 JavaScript 中的场景中使用。 58 | */ 59 | void decrementRefCount(); 60 | 61 | HashMap toMap(); 62 | 63 | ArrayList toArray(); 64 | 65 | HashMap toMap(MapFilter filter); 66 | ArrayList toArray(MapFilter filter); 67 | Map toMap(MapFilter filter, Object extra, MapCreator mapCreator); 68 | ArrayList toArray(MapFilter filter, Object extra, MapCreator mapCreator); 69 | } 70 | -------------------------------------------------------------------------------- /wrapper-java/src/main/java/com/whl/quickjs/wrapper/JSObjectCreator.java: -------------------------------------------------------------------------------- 1 | package com.whl.quickjs.wrapper; 2 | 3 | /** 4 | * Created by Harlon Wang on 2024/2/15. 5 | */ 6 | public interface JSObjectCreator { 7 | JSObject newObject(QuickJSContext context, long pointer); 8 | JSArray newArray(QuickJSContext context, long pointer); 9 | JSFunction newFunction(QuickJSContext context, long pointer, long thisPointer, int thisPointerTag); 10 | } 11 | -------------------------------------------------------------------------------- /wrapper-java/src/main/java/com/whl/quickjs/wrapper/MapCreator.java: -------------------------------------------------------------------------------- 1 | package com.whl.quickjs.wrapper; 2 | 3 | import java.util.Map; 4 | 5 | /** 6 | * Created by Harlon Wang on 2025/1/21. 7 | */ 8 | public interface MapCreator { 9 | Map get(); 10 | } 11 | -------------------------------------------------------------------------------- /wrapper-java/src/main/java/com/whl/quickjs/wrapper/MapFilter.java: -------------------------------------------------------------------------------- 1 | package com.whl.quickjs.wrapper; 2 | 3 | /** 4 | * Created by Harlon Wang on 2024/9/27. 5 | */ 6 | public interface MapFilter { 7 | 8 | /** 9 | * toMap 转换期间是否需要过滤指定 key 10 | * @param key 需要过滤的 key 11 | * @param pointer 当前 key 所属对象的指针地址 12 | * @param extra 扩展参数,如果你需要一些额外信息,可通过 toMap(..., extra) 传递该参数 13 | * @return 是否需要过滤该 key 14 | */ 15 | boolean shouldSkipKey(String key, long pointer, Object extra); 16 | 17 | } 18 | -------------------------------------------------------------------------------- /wrapper-java/src/main/java/com/whl/quickjs/wrapper/ModuleLoader.java: -------------------------------------------------------------------------------- 1 | package com.whl.quickjs.wrapper; 2 | 3 | /** 4 | * Created by Harlon Wang on 2023/8/26. 5 | * 该类仅提供给 Native 层调用 6 | */ 7 | public abstract class ModuleLoader { 8 | /** 9 | * 模块加载模式: 10 | * True 会调用 {@link #getModuleBytecode(String)} 11 | * False 会调用 {@link #getModuleStringCode(String)} 12 | * @return 是否字节码模式 13 | */ 14 | public abstract boolean isBytecodeMode(); 15 | 16 | /** 17 | * 获取字节码代码内容 18 | * @param moduleName 模块路径名,例如 "xxx.js" 19 | * @return 代码内容 20 | */ 21 | public abstract byte[] getModuleBytecode(String moduleName); 22 | 23 | /** 24 | * 获取字符串代码内容 25 | * @param moduleName 模块路径名,例如 "xxx.js" 26 | * @return 代码内容 27 | */ 28 | public abstract String getModuleStringCode(String moduleName); 29 | 30 | 31 | /** 32 | * 该方法返回结果会作为 moduleName 参数给到 {@link #getModuleBytecode(String)} 33 | * 或者 {@link #getModuleStringCode(String)} 中使用,默认返回 moduleName。 34 | * 一般可以在这里对模块名称进行转换处理。 35 | * @param baseModuleName 使用 Import 的所在模块名称 36 | * @param moduleName 需要加载的模块名称 37 | * @return 模块名称 38 | */ 39 | public String moduleNormalizeName(String baseModuleName, String moduleName) { 40 | return moduleName; 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /wrapper-java/src/main/java/com/whl/quickjs/wrapper/QuickJSArray.java: -------------------------------------------------------------------------------- 1 | package com.whl.quickjs.wrapper; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashMap; 5 | import java.util.HashSet; 6 | import java.util.Map; 7 | 8 | /** 9 | * Created by Harlon Wang on 2024/2/13. 10 | */ 11 | public class QuickJSArray extends QuickJSObject implements JSArray { 12 | 13 | public QuickJSArray(QuickJSContext context, long pointer) { 14 | super(context, pointer); 15 | } 16 | 17 | @Override 18 | public int length() { 19 | checkRefCountIsZero(); 20 | return getContext().length(this); 21 | } 22 | 23 | @Override 24 | public Object get(int index) { 25 | checkRefCountIsZero(); 26 | return getContext().get(this, index); 27 | } 28 | 29 | @Override 30 | public void set(Object value, int index) { 31 | checkRefCountIsZero(); 32 | getContext().set(this, value, index); 33 | } 34 | 35 | @Override 36 | public HashMap toMap() { 37 | return toMap(null); 38 | } 39 | 40 | @Override 41 | public HashMap toMap(MapFilter filter) { 42 | return (HashMap) toMap(filter, null, null); 43 | } 44 | 45 | @Override 46 | public ArrayList toArray() { 47 | return toArray(null); 48 | } 49 | 50 | @Override 51 | public ArrayList toArray(MapFilter filter) { 52 | return toArray(filter, null, HashMap::new); 53 | } 54 | 55 | @Override 56 | public ArrayList toArray(MapFilter filter, Object extra, MapCreator creator) { 57 | ArrayList arrayList = new ArrayList<>(length()); 58 | HashMap circulars = new HashMap<>(); 59 | convertToMap(this, arrayList, circulars, filter, extra, creator); 60 | circulars.clear(); 61 | return arrayList; 62 | } 63 | 64 | @Override 65 | public Map toMap(MapFilter filter, Object extra, MapCreator mapCreator) { 66 | throw new UnsupportedOperationException("Array types are not yet supported for conversion to map. You should use toArray."); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /wrapper-java/src/main/java/com/whl/quickjs/wrapper/QuickJSContext.java: -------------------------------------------------------------------------------- 1 | package com.whl.quickjs.wrapper; 2 | 3 | import java.io.Closeable; 4 | import java.io.File; 5 | import java.util.ArrayList; 6 | import java.util.HashMap; 7 | import java.util.Iterator; 8 | import java.util.List; 9 | 10 | public class QuickJSContext implements Closeable { 11 | 12 | @Override 13 | public void close() throws QuickJSException { 14 | destroy(); 15 | } 16 | 17 | public interface Console { 18 | void log(String info); 19 | void info(String info); 20 | void warn(String info); 21 | void error(String info); 22 | } 23 | 24 | public interface LeakDetectionListener { 25 | void notifyLeakDetected(JSObject leak, String stringValue); 26 | } 27 | 28 | public static abstract class DefaultModuleLoader extends ModuleLoader { 29 | 30 | @Override 31 | public boolean isBytecodeMode() { 32 | return false; 33 | } 34 | 35 | @Override 36 | public byte[] getModuleBytecode(String moduleName) { 37 | return null; 38 | } 39 | } 40 | 41 | public static abstract class BytecodeModuleLoader extends ModuleLoader { 42 | @Override 43 | public boolean isBytecodeMode() { 44 | return true; 45 | } 46 | 47 | @Override 48 | public String getModuleStringCode(String moduleName) { 49 | return null; 50 | } 51 | } 52 | 53 | private static final String UNKNOWN_FILE = "unknown.js"; 54 | 55 | public static QuickJSContext create() { 56 | return new QuickJSContext(new JSObjectCreator() { 57 | @Override 58 | public JSObject newObject(QuickJSContext context, long pointer) { 59 | return new QuickJSObject(context, pointer); 60 | } 61 | 62 | @Override 63 | public JSArray newArray(QuickJSContext context, long pointer) { 64 | return new QuickJSArray(context, pointer); 65 | } 66 | 67 | @Override 68 | public JSFunction newFunction(QuickJSContext context, long pointer, long thisPointer, int thisPointerTag) { 69 | return new QuickJSFunction(context, pointer, thisPointer, thisPointerTag); 70 | } 71 | }); 72 | } 73 | 74 | public static QuickJSContext create(JSObjectCreator creator) { 75 | return new QuickJSContext(creator); 76 | } 77 | 78 | public boolean isLiveObject(JSObject jsObj) { 79 | return isLiveObject(runtime, jsObj.getPointer()); 80 | } 81 | 82 | public void setConsole(Console console) { 83 | if (console == null) { 84 | return; 85 | } 86 | 87 | JSObject consoleObj = getGlobalObject().getJSObject("console"); 88 | consoleObj.setProperty("stdout", args -> { 89 | if (args.length == 2) { 90 | String level = (String) args[0]; 91 | String info = (String) args[1]; 92 | switch (level) { 93 | case "info": 94 | console.info(info); 95 | break; 96 | case "warn": 97 | console.warn(info); 98 | break; 99 | case "error": 100 | console.error(info); 101 | break; 102 | case "log": 103 | case "debug": 104 | default: 105 | console.log(info); 106 | break; 107 | } 108 | } 109 | 110 | return null; 111 | }); 112 | consoleObj.release(); 113 | } 114 | 115 | public void setMaxStackSize(int maxStackSize) { 116 | setMaxStackSize(runtime, maxStackSize); 117 | } 118 | 119 | public void setGCThreshold(int thresholdSize) { 120 | setGCThreshold(runtime, thresholdSize); 121 | } 122 | 123 | public void runGC() { 124 | runGC(runtime); 125 | } 126 | 127 | public void setMemoryLimit(int memoryLimitSize) { 128 | setMemoryLimit(runtime, memoryLimitSize); 129 | } 130 | 131 | // Return the byte size. 132 | public long getMemoryUsedSize() { 133 | return getMemoryUsedSize(runtime); 134 | } 135 | 136 | public void dumpMemoryUsage(File target) { 137 | checkSameThread(); 138 | checkDestroyed(); 139 | String fileName = null; 140 | if (target != null && target.exists()) { 141 | fileName = target.getAbsolutePath(); 142 | } 143 | 144 | dumpMemoryUsage(runtime, fileName); 145 | } 146 | 147 | // will use stdout to print. 148 | public void dumpMemoryUsage() { 149 | dumpMemoryUsage(null); 150 | } 151 | 152 | public void dumpObjects(File target) { 153 | checkSameThread(); 154 | checkDestroyed(); 155 | String fileName = null; 156 | if (target != null && target.exists()) { 157 | fileName = target.getAbsolutePath(); 158 | } 159 | 160 | dumpObjects(runtime, fileName); 161 | } 162 | 163 | public JSObjectCreator getCreator() { 164 | return creator; 165 | } 166 | 167 | // will use stdout to print. 168 | public void dumpObjects() { 169 | dumpObjects(null); 170 | } 171 | 172 | private final long runtime; 173 | private final long context; 174 | private final long currentThreadId; 175 | private boolean destroyed = false; 176 | private final HashMap callFunctionMap = new HashMap<>(); 177 | 178 | private ModuleLoader moduleLoader; 179 | private JSObject globalObject; 180 | private final JSObjectCreator creator; 181 | private final List objectRecords = new ArrayList<>(); 182 | private LeakDetectionListener leakDetectionListener; 183 | private boolean enableStackTrace = false; 184 | 185 | private QuickJSContext(JSObjectCreator creator) { 186 | try { 187 | // 这里代理一层 creator,用来记录 js 对象. 188 | this.creator = new JSObjectCreator() { 189 | @Override 190 | public JSObject newObject(QuickJSContext c, long pointer) { 191 | JSObject o = creator.newObject(c, pointer); 192 | if (enableStackTrace) { 193 | o.setStackTrace(new Throwable()); 194 | } 195 | objectRecords.add(o); 196 | return o; 197 | } 198 | 199 | @Override 200 | public JSArray newArray(QuickJSContext c, long pointer) { 201 | JSArray o = creator.newArray(c, pointer); 202 | if (enableStackTrace) { 203 | o.setStackTrace(new Throwable()); 204 | } 205 | objectRecords.add(o); 206 | return o; 207 | } 208 | 209 | @Override 210 | public JSFunction newFunction(QuickJSContext c, long pointer, long thisPointer, int thisPointerTag) { 211 | JSFunction o = creator.newFunction(c, pointer, thisPointer, thisPointerTag); 212 | if (enableStackTrace) { 213 | o.setStackTrace(new Throwable()); 214 | } 215 | objectRecords.add(o); 216 | return o; 217 | } 218 | }; 219 | runtime = createRuntime(); 220 | context = createContext(runtime); 221 | } catch (UnsatisfiedLinkError e) { 222 | throw new QuickJSException("The so library must be initialized before createContext! QuickJSLoader.init should be called on the Android platform. In the JVM, you need to manually call System.loadLibrary"); 223 | } 224 | currentThreadId = Thread.currentThread().getId(); 225 | } 226 | 227 | public void setEnableStackTrace(boolean enableStackTrace) { 228 | this.enableStackTrace = enableStackTrace; 229 | } 230 | 231 | private void checkSameThread() { 232 | boolean isSameThread = currentThreadId == Thread.currentThread().getId(); 233 | if (!isSameThread) { 234 | throw new QuickJSException("Must be call same thread in QuickJSContext.create!"); 235 | } 236 | } 237 | 238 | public long getCurrentThreadId() { 239 | return currentThreadId; 240 | } 241 | 242 | public void setModuleLoader(ModuleLoader moduleLoader) { 243 | checkSameThread(); 244 | checkDestroyed(); 245 | 246 | if (moduleLoader == null) { 247 | throw new NullPointerException("The moduleLoader can not be null!"); 248 | } 249 | 250 | this.moduleLoader = moduleLoader; 251 | } 252 | 253 | public ModuleLoader getModuleLoader() { 254 | return moduleLoader; 255 | } 256 | 257 | private void checkDestroyed() { 258 | if (destroyed) { 259 | throw new QuickJSException("Can not called this after QuickJSContext was destroyed!"); 260 | } 261 | } 262 | 263 | public Object evaluate(String script) { 264 | return evaluate(script, UNKNOWN_FILE); 265 | } 266 | 267 | public Object evaluate(String script, String fileName) { 268 | if (script == null) { 269 | throw new NullPointerException("Script cannot be null with " + fileName); 270 | } 271 | 272 | checkSameThread(); 273 | checkDestroyed(); 274 | return evaluate(context, script, fileName); 275 | } 276 | 277 | public JSObject getGlobalObject() { 278 | checkSameThread(); 279 | checkDestroyed(); 280 | 281 | if (globalObject == null) { 282 | globalObject = getGlobalObject(context); 283 | } 284 | 285 | return globalObject; 286 | } 287 | 288 | public void setLeakDetectionListener(LeakDetectionListener leakDetectionListener) { 289 | this.leakDetectionListener = leakDetectionListener; 290 | } 291 | 292 | public void destroy() { 293 | checkSameThread(); 294 | checkDestroyed(); 295 | 296 | callFunctionMap.clear(); 297 | releaseObjectRecords(); 298 | objectRecords.clear(); 299 | destroyContext(context); 300 | destroyed = true; 301 | } 302 | 303 | public void releaseObjectRecords() { 304 | releaseObjectRecords(true); 305 | } 306 | 307 | public void releaseObjectRecords(boolean needRelease) { 308 | JSFunction format = getGlobalObject().getJSFunction("format"); 309 | 310 | // 检测是否有未被释放引用的对象,如果有的话,根据计数释放一下 311 | Iterator objectIterator = objectRecords.iterator(); 312 | while (objectIterator.hasNext()) { 313 | JSObject object = objectIterator.next(); 314 | 315 | // 这里需要过滤掉 getGlobalObject 和 format 316 | // 1. getGlobalObject 全局对象不会主动释放,引擎销毁会回收 317 | // 2. format 用来格式化内容,会在迭代完释放掉,这里过滤掉 318 | if (!object.isRefCountZero() && object != getGlobalObject() && object != format) { 319 | int refCount = object.getRefCount(); 320 | if (leakDetectionListener != null) { 321 | String value = (String) format.call(object); 322 | leakDetectionListener.notifyLeakDetected(object, value); 323 | } 324 | 325 | if (needRelease) { 326 | for (int j = 0; j < refCount; j++) { 327 | // 这里不能直接调用 object.release 方法,因为 release 里会调用 list.remove 导致并发修改异常 328 | object.decrementRefCount(); 329 | freeValue(context, object.getPointer()); 330 | } 331 | 332 | if (object.getRefCount() == 0) { 333 | objectIterator.remove(); 334 | } 335 | } 336 | } 337 | } 338 | 339 | format.release(); 340 | } 341 | 342 | public List getObjectRecords() { 343 | return objectRecords; 344 | } 345 | 346 | public String stringify(JSObject jsObj) { 347 | checkSameThread(); 348 | checkDestroyed(); 349 | return stringify(context, jsObj.getPointer()); 350 | } 351 | 352 | public Object getProperty(JSObject jsObj, String name) { 353 | checkSameThread(); 354 | checkDestroyed(); 355 | return getProperty(context, jsObj.getPointer(), name); 356 | } 357 | 358 | public void setProperty(JSObject jsObj, String name, Object value) { 359 | checkSameThread(); 360 | checkDestroyed(); 361 | 362 | if (value instanceof JSCallFunction) { 363 | // Todo 优化:可以只传 callFunctionId 给到 JNI. 364 | putCallFunction((JSCallFunction) value); 365 | } 366 | 367 | setProperty(context, jsObj.getPointer(), name, value); 368 | } 369 | 370 | private void putCallFunction(JSCallFunction callFunction) { 371 | int callFunctionId = callFunction.hashCode(); 372 | callFunctionMap.put(callFunctionId, callFunction); 373 | } 374 | 375 | /** 376 | * 该方法只提供给 Native 层回调. 377 | * @param callFunctionId JSCallFunction 对象标识 378 | */ 379 | public void removeCallFunction(int callFunctionId) { 380 | callFunctionMap.remove(callFunctionId); 381 | } 382 | 383 | /** 384 | * 该方法只提供给 Native 层回调. 385 | * @param callFunctionId JSCallFunction 对象标识 386 | * @param args JS 到 Java 的参数映射 387 | */ 388 | public Object callFunctionBack(int callFunctionId, Object... args) { 389 | checkSameThread(); 390 | checkDestroyed(); 391 | 392 | JSCallFunction callFunction = callFunctionMap.get(callFunctionId); 393 | Object ret = callFunction.call(args); 394 | if (ret instanceof JSCallFunction) { 395 | putCallFunction((JSCallFunction) ret); 396 | } 397 | 398 | if (ret instanceof JSObject) { 399 | // 注意:JSObject 对象作为参数返回到️ JavaScript 中,不需要调用 release 方法, 400 | // JS 引擎会进行 free,但是这里需要手动对 JSObject 对象的计数减一。 401 | ((JSObject) ret).decrementRefCount(); 402 | 403 | if (((JSObject) ret).getRefCount() == 0) { 404 | objectRecords.remove(((JSObject) ret)); 405 | } 406 | } 407 | 408 | return ret; 409 | } 410 | 411 | /** 412 | * JS 引擎层的对象计数减一。 413 | */ 414 | public void freeValue(JSObject jsObj) { 415 | checkSameThread(); 416 | checkDestroyed(); 417 | 418 | freeValue(context, jsObj.getPointer()); 419 | 420 | // todo 如果计数为 0,从 objectRecords 里移除掉 421 | if (jsObj.getRefCount() == 0) { 422 | objectRecords.remove(jsObj); 423 | } 424 | } 425 | 426 | /** 427 | * @VisibleForTesting 428 | * 该方法仅供单元测试使用 429 | */ 430 | int getCallFunctionMapSize() { 431 | return callFunctionMap.size(); 432 | } 433 | 434 | /** 435 | * JS 引擎层的对象计数加一。 436 | */ 437 | private void dupValue(JSObject jsObj) { 438 | checkSameThread(); 439 | checkDestroyed(); 440 | 441 | dupValue(context, jsObj.getPointer()); 442 | } 443 | 444 | public int length(JSArray jsArray) { 445 | checkSameThread(); 446 | checkDestroyed(); 447 | 448 | // todo 待优化 449 | if (!isLiveObject(jsArray)) { 450 | return 0; 451 | } 452 | 453 | return length(context, jsArray.getPointer()); 454 | } 455 | 456 | public Object get(JSArray jsArray, int index) { 457 | checkSameThread(); 458 | checkDestroyed(); 459 | 460 | return get(context, jsArray.getPointer(), index); 461 | } 462 | 463 | public void set(JSArray jsArray, Object value, int index) { 464 | checkSameThread(); 465 | checkDestroyed(); 466 | 467 | set(context, jsArray.getPointer(), value, index); 468 | } 469 | 470 | Object call(JSObject func, long objPointer, int thisPointerTag, Object... args) { 471 | checkSameThread(); 472 | checkDestroyed(); 473 | 474 | for (int i = 0; i < args.length; i++) { 475 | Object arg = args[i]; 476 | if (arg instanceof JSCallFunction) { 477 | putCallFunction((JSCallFunction) arg); 478 | } 479 | } 480 | 481 | return call(context, func.getPointer(), objPointer, thisPointerTag, args); 482 | } 483 | 484 | /** 485 | * Automatically manage the release of objects, 486 | * the hold method is equivalent to call the 487 | * dupValue and freeDupValue methods with NativeCleaner. 488 | */ 489 | public void hold(JSObject jsObj) { 490 | checkSameThread(); 491 | checkDestroyed(); 492 | 493 | dupValue(jsObj); 494 | } 495 | 496 | public JSObject createNewJSObject() { 497 | return parseJSON("{}"); 498 | } 499 | 500 | public JSArray createNewJSArray() { 501 | return (JSArray) parseJSON("[]"); 502 | } 503 | 504 | /** 505 | * Use {@link #parse(String)} replace. 506 | */ 507 | @Deprecated 508 | public JSObject parseJSON(String json) { 509 | checkSameThread(); 510 | checkDestroyed(); 511 | 512 | Object obj = parseJSON(context, json); 513 | if (!(obj instanceof JSObject)) { 514 | throw new QuickJSException("Only parse json with valid format, must be start with '{', if it contains other case, use parse(String) replace."); 515 | } 516 | 517 | return (JSObject) obj; 518 | } 519 | 520 | public Object parse(String json) { 521 | checkSameThread(); 522 | checkDestroyed(); 523 | return parseJSON(context, json); 524 | } 525 | 526 | public byte[] compile(String script) { 527 | return compile(script, UNKNOWN_FILE); 528 | } 529 | 530 | public byte[] compile(String script, String fileName) { 531 | if (script == null) { 532 | throw new NullPointerException("Script cannot be null with " + fileName); 533 | } 534 | 535 | checkSameThread(); 536 | checkDestroyed(); 537 | 538 | return compile(context, script, fileName, false); 539 | } 540 | 541 | public byte[] compileModule(String script) { 542 | return compileModule(script, UNKNOWN_FILE); 543 | } 544 | 545 | public byte[] compileModule(String script, String fileName) { 546 | if (script == null) { 547 | throw new NullPointerException("Script cannot be null with " + fileName); 548 | } 549 | 550 | checkSameThread(); 551 | checkDestroyed(); 552 | 553 | return compile(context, script, fileName, true); 554 | } 555 | 556 | public Object execute(byte[] code) { 557 | if (code == null) { 558 | throw new NullPointerException("Bytecode cannot be null"); 559 | } 560 | 561 | checkSameThread(); 562 | checkDestroyed(); 563 | 564 | return execute(context, code); 565 | } 566 | 567 | public Object evaluateModule(String script, String moduleName) { 568 | if (script == null) { 569 | throw new NullPointerException("Script cannot be null with " + moduleName); 570 | } 571 | 572 | checkSameThread(); 573 | checkDestroyed(); 574 | return evaluateModule(context, script, moduleName); 575 | } 576 | 577 | public Object evaluateModule(String script) { 578 | return evaluateModule(script, UNKNOWN_FILE); 579 | } 580 | 581 | public void throwJSException(String error) { 582 | // throw $error; 583 | String errorScript = "throw " + "\"" + error + "\"" + ";"; 584 | evaluate(errorScript); 585 | } 586 | 587 | public Object getOwnPropertyNames(JSObject object) { 588 | return getOwnPropertyNames(context, object.getPointer()); 589 | } 590 | 591 | // runtime 592 | private native long createRuntime(); 593 | private native void setMaxStackSize(long runtime, int size); // The default is 1024 * 256, and 0 means unlimited. 594 | private native boolean isLiveObject(long runtime, long objValue); 595 | private native void runGC(long runtime); 596 | private native void setMemoryLimit(long runtime, int size); 597 | private native void dumpMemoryUsage(long runtime, String fileName); 598 | private native void dumpObjects(long runtime, String fileName); 599 | private native long getMemoryUsedSize(long runtime); 600 | private native void setGCThreshold(long runtime, int size); 601 | 602 | // context 603 | private native long createContext(long runtime); 604 | private native Object evaluate(long context, String script, String fileName); 605 | private native Object evaluateModule(long context, String script, String fileName); 606 | private native JSObject getGlobalObject(long context); 607 | private native Object call(long context, long func, long thisObj, int thisObjTag, Object[] args); 608 | private native Object getProperty(long context, long objValue, String name); 609 | private native void setProperty(long context, long objValue, String name, Object value); 610 | private native String stringify(long context, long objValue); 611 | private native int length(long context, long objValue); 612 | private native Object get(long context, long objValue, int index); 613 | private native void set(long context, long objValue, Object value, int index); 614 | private native void freeValue(long context, long objValue); 615 | private native void dupValue(long context, long objValue); 616 | private native void freeDupValue(long context, long objValue); 617 | private native Object parseJSON(long context, String json); 618 | private native byte[] compile(long context, String sourceCode, String fileName, boolean isModule); // Bytecode compile 619 | private native Object execute(long context, byte[] bytecode); // Bytecode execute 620 | private native Object getOwnPropertyNames(long context, long objValue); 621 | 622 | // destroy context and runtime 623 | private native void destroyContext(long context); 624 | } 625 | -------------------------------------------------------------------------------- /wrapper-java/src/main/java/com/whl/quickjs/wrapper/QuickJSException.java: -------------------------------------------------------------------------------- 1 | package com.whl.quickjs.wrapper; 2 | 3 | /** 4 | * Created by Harlon Wang on 2022/2/8. 5 | */ 6 | public class QuickJSException extends RuntimeException { 7 | 8 | private final boolean jsError; 9 | 10 | public QuickJSException(String message) { 11 | this(message, false); 12 | } 13 | 14 | public QuickJSException(String message, boolean jsError) { 15 | super(message); 16 | this.jsError = jsError; 17 | } 18 | 19 | public boolean isJSError() { 20 | return jsError; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /wrapper-java/src/main/java/com/whl/quickjs/wrapper/QuickJSFunction.java: -------------------------------------------------------------------------------- 1 | package com.whl.quickjs.wrapper; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashMap; 5 | 6 | /** 7 | * Created by Harlon Wang on 2024/2/12. 8 | */ 9 | public class QuickJSFunction extends QuickJSObject implements JSFunction { 10 | 11 | /** 12 | * 函数执行状态 13 | */ 14 | enum Status { 15 | NOT_CALLED, 16 | CALLING, 17 | CALLED 18 | } 19 | private int stashTimes = 0; 20 | private Status currentStatus = Status.NOT_CALLED; 21 | 22 | private final long thisPointer; 23 | private final int thisPointerTag; 24 | 25 | public QuickJSFunction(QuickJSContext context, long pointer, long thisPointer, int thisPointerTag) { 26 | super(context, pointer); 27 | this.thisPointer = thisPointer; 28 | this.thisPointerTag = thisPointerTag; 29 | } 30 | 31 | @Override 32 | public void release() { 33 | // call 函数未执行完,触发了 release 操作,会导致 quickjs 野指针异常, 34 | // 这里暂存一下,待函数执行完,才执行 release。 35 | if (currentStatus == Status.CALLING) { 36 | stashTimes++; 37 | return; 38 | } 39 | super.release(); 40 | } 41 | 42 | @Override 43 | public Object call(Object... args) { 44 | checkRefCountIsZero(); 45 | 46 | currentStatus = Status.CALLING; 47 | Object ret; 48 | try { 49 | ret = getContext().call(this, thisPointer, thisPointerTag, args); 50 | } finally { 51 | // call 可能会抛出异常,需要保障以下代码被执行,不然因为状态不对,导致无法正常 release。 52 | currentStatus = Status.CALLED; 53 | 54 | if (stashTimes > 0) { 55 | // 如果有暂存,这里需要恢复下 release 操作 56 | for (int i = 0; i < stashTimes; i++) { 57 | release(); 58 | } 59 | stashTimes = 0; 60 | } 61 | } 62 | 63 | return ret; 64 | } 65 | 66 | @Override 67 | public void callVoid(Object... args) { 68 | Object ret = call(args); 69 | if (ret instanceof JSObject) { 70 | ((JSObject) ret).release(); 71 | } 72 | } 73 | 74 | @Override 75 | public HashMap toMap() { 76 | throw new UnsupportedOperationException("JSFunction types do not support conversion to map or array."); 77 | } 78 | 79 | @Override 80 | public ArrayList toArray() { 81 | throw new UnsupportedOperationException("JSFunction types do not support conversion to map or array."); 82 | } 83 | } -------------------------------------------------------------------------------- /wrapper-java/src/main/java/com/whl/quickjs/wrapper/QuickJSObject.java: -------------------------------------------------------------------------------- 1 | package com.whl.quickjs.wrapper; 2 | 3 | import java.lang.reflect.InvocationTargetException; 4 | import java.lang.reflect.Method; 5 | import java.util.ArrayList; 6 | import java.util.Arrays; 7 | import java.util.HashMap; 8 | import java.util.HashSet; 9 | import java.util.Map; 10 | 11 | /** 12 | * Created by Harlon Wang on 2024/2/12. 13 | */ 14 | public class QuickJSObject implements JSObject { 15 | 16 | private final QuickJSContext context; 17 | private final long pointer; 18 | private int refCount; 19 | private Throwable stackTrace; 20 | 21 | public QuickJSObject(QuickJSContext context, long pointer) { 22 | this.context = context; 23 | this.pointer = pointer; 24 | refCount++; 25 | } 26 | 27 | @Override 28 | public long getPointer() { 29 | return pointer; 30 | } 31 | 32 | @Override 33 | public QuickJSContext getContext() { 34 | return context; 35 | } 36 | 37 | @Override 38 | public Object getProperty(String name) { 39 | checkRefCountIsZero(); 40 | return context.getProperty(this, name); 41 | } 42 | 43 | @Override 44 | public String getStringProperty(String name) { 45 | return getString(name); 46 | } 47 | 48 | @Override 49 | public void setStackTrace(Throwable stackTrace) { 50 | this.stackTrace = stackTrace; 51 | } 52 | 53 | @Override 54 | public Throwable getStackTrace() { 55 | return stackTrace; 56 | } 57 | 58 | @Override 59 | public void setProperty(String name, String value) { 60 | setPropertyObject(name, value); 61 | } 62 | 63 | @Override 64 | public void setProperty(String name, int value) { 65 | setPropertyObject(name, value); 66 | } 67 | 68 | @Override 69 | public void setProperty(String name, long value) { 70 | setPropertyObject(name, value); 71 | } 72 | 73 | @Override 74 | public void setProperty(String name, JSObject value) { 75 | setPropertyObject(name, value); 76 | } 77 | 78 | @Override 79 | public void setProperty(String name, boolean value) { 80 | setPropertyObject(name, value); 81 | } 82 | 83 | @Override 84 | public void setProperty(String name, double value) { 85 | setPropertyObject(name, value); 86 | } 87 | 88 | @Override 89 | public void setProperty(String name, byte[] value) { 90 | setPropertyObject(name, value); 91 | } 92 | 93 | @Override 94 | public void setProperty(String name, JSCallFunction value) { 95 | setPropertyObject(name, value); 96 | } 97 | 98 | private void setPropertyObject(String name, Object o) { 99 | checkRefCountIsZero(); 100 | context.setProperty(this, name, o); 101 | } 102 | 103 | @Override 104 | public void setProperty(String name, Class clazz) { 105 | Object javaObj = null; 106 | try { 107 | javaObj = clazz.newInstance(); 108 | } catch (IllegalAccessException e) { 109 | e.printStackTrace(); 110 | } catch (InstantiationException e) { 111 | e.printStackTrace(); 112 | } 113 | 114 | if (javaObj == null) { 115 | throw new NullPointerException("The JavaObj cannot be null. An error occurred in newInstance!"); 116 | } 117 | 118 | JSObject jsObj = context.createNewJSObject(); 119 | Method[] methods = clazz.getMethods(); 120 | for (Method method : methods) { 121 | if (method.isAnnotationPresent(JSMethod.class)) { 122 | Object finalJavaObj = javaObj; 123 | jsObj.setProperty(method.getName(), args -> { 124 | try { 125 | return method.invoke(finalJavaObj, args); 126 | } catch (IllegalAccessException e) { 127 | e.printStackTrace(); 128 | } catch (InvocationTargetException e) { 129 | e.printStackTrace(); 130 | } 131 | return null; 132 | }); 133 | } 134 | } 135 | 136 | setProperty(name, jsObj); 137 | jsObj.release(); 138 | } 139 | 140 | @Override 141 | public String getString(String name) { 142 | Object value = getProperty(name); 143 | return value instanceof String ? (String) value : null; 144 | } 145 | 146 | @Override 147 | public Integer getIntProperty(String name) { 148 | return getInteger(name); 149 | } 150 | 151 | @Override 152 | public Integer getInteger(String name) { 153 | Object value = getProperty(name); 154 | return value instanceof Integer ? (Integer) value : null; 155 | } 156 | 157 | @Override 158 | public Boolean getBooleanProperty(String name) { 159 | return getBoolean(name); 160 | } 161 | 162 | @Override 163 | public Boolean getBoolean(String name) { 164 | Object value = getProperty(name); 165 | return value instanceof Boolean ? (Boolean) value : null; 166 | } 167 | 168 | @Override 169 | public Double getDoubleProperty(String name) { 170 | return getDouble(name); 171 | } 172 | 173 | @Override 174 | public Double getDouble(String name) { 175 | Object value = getProperty(name); 176 | return value instanceof Double ? (Double) value : null; 177 | } 178 | 179 | @Override 180 | public Long getLong(String name) { 181 | Object value = getProperty(name); 182 | return value instanceof Long ? (Long) value : null; 183 | } 184 | 185 | @Override 186 | public byte[] getBytes(String name) { 187 | Object value = getProperty(name); 188 | return value instanceof byte[] ? (byte[]) value : null; 189 | } 190 | 191 | @Override 192 | public JSObject getJSObjectProperty(String name) { 193 | return getJSObject(name); 194 | } 195 | 196 | @Override 197 | public JSObject getJSObject(String name) { 198 | Object value = getProperty(name); 199 | return value instanceof JSObject ? (JSObject) value : null; 200 | } 201 | 202 | @Override 203 | public JSFunction getJSFunctionProperty(String name) { 204 | return getJSFunction(name); 205 | } 206 | 207 | @Override 208 | public JSFunction getJSFunction(String name) { 209 | Object value = getProperty(name); 210 | return value instanceof JSFunction ? (JSFunction) value : null; 211 | } 212 | 213 | @Override 214 | public JSArray getJSArrayProperty(String name) { 215 | return getJSArray(name); 216 | } 217 | 218 | @Override 219 | public JSArray getJSArray(String name) { 220 | Object value = getProperty(name); 221 | return value instanceof JSArray ? (JSArray) value : null; 222 | } 223 | 224 | @Override 225 | public JSArray getOwnPropertyNames() { 226 | return getNames(); 227 | } 228 | 229 | @Override 230 | public JSArray getNames() { 231 | checkRefCountIsZero(); 232 | return (JSArray) context.getOwnPropertyNames(this); 233 | } 234 | 235 | @Override 236 | public void release() { 237 | if (isRefCountZero()) { 238 | return; 239 | } 240 | refCount--; 241 | context.freeValue(this); 242 | } 243 | 244 | @Override 245 | public void hold() { 246 | checkRefCountIsZero(); 247 | refCount++; 248 | context.hold(this); 249 | } 250 | 251 | @Override 252 | public void decrementRefCount() { 253 | checkRefCountIsZero(); 254 | refCount--; 255 | } 256 | 257 | @Override 258 | public HashMap toMap() { 259 | return toMap(null); 260 | } 261 | 262 | @Override 263 | public ArrayList toArray() { 264 | return toArray(null); 265 | } 266 | 267 | @Override 268 | public HashMap toMap(MapFilter filter) { 269 | return (HashMap) toMap(filter, null, HashMap::new); 270 | } 271 | 272 | @Override 273 | public Map toMap(MapFilter filter, Object extra, MapCreator mapCreator) { 274 | Map objectMap = mapCreator.get(); 275 | HashMap circulars = new HashMap<>(); 276 | convertToMap(this, objectMap, circulars, filter, extra, mapCreator); 277 | circulars.clear(); 278 | return objectMap; 279 | } 280 | 281 | @Override 282 | public ArrayList toArray(MapFilter filter, Object extra, MapCreator mapCreator) { 283 | throw new UnsupportedOperationException("Object types are not yet supported for conversion to array. You should use toMap."); 284 | } 285 | 286 | @Override 287 | public ArrayList toArray(MapFilter filter) { 288 | return toArray(filter, null, null); 289 | } 290 | 291 | protected void convertToMap(Object target, Object map, HashMap circulars, MapFilter filter, Object extra, MapCreator mapCreator) { 292 | circulars.put(((JSObject) target).getPointer(), map); 293 | 294 | boolean isArray = target instanceof JSArray; 295 | JSArray array = isArray ? (JSArray) target : ((JSObject) target).getNames(); 296 | int length = array.length(); 297 | for (int i = 0; i < length; i++) { 298 | String key = null; 299 | Object value; 300 | if (isArray) { 301 | value = array.get(i); 302 | } else { 303 | key = (String) array.get(i); 304 | if (filter != null && filter.shouldSkipKey(key, pointer, extra)) { 305 | continue; 306 | } 307 | value = ((JSObject) target).getProperty(key); 308 | } 309 | 310 | if (value instanceof JSObject) { 311 | long pointer = ((JSObject) value).getPointer(); 312 | if (circulars.containsKey(pointer)) { 313 | // Circular reference objects. 314 | Object refValue = circulars.get(pointer); 315 | if (map instanceof Map) { 316 | ((Map) map).put(key, refValue); 317 | } else if (map instanceof ArrayList){ 318 | ((ArrayList) map).add(refValue); 319 | } 320 | continue; 321 | } 322 | } 323 | 324 | if (value instanceof JSFunction) { 325 | // Unsupported type. 326 | ((JSFunction) value).release(); 327 | continue; 328 | } 329 | 330 | if (value instanceof JSArray) { 331 | ArrayList list = new ArrayList<>(((JSArray) value).length()); 332 | convertToMap(value, list, circulars, filter, extra, mapCreator); 333 | if (map instanceof Map) { 334 | ((Map) map).put(key, list); 335 | } else if (map instanceof ArrayList){ 336 | ((ArrayList) map).add(list); 337 | } 338 | ((JSArray) value).release(); 339 | continue; 340 | } 341 | 342 | if (value instanceof JSObject) { 343 | Map valueMap = mapCreator.get(); 344 | convertToMap(value, valueMap, circulars, filter, extra, mapCreator); 345 | if (map instanceof Map) { 346 | ((Map) map).put(key, valueMap); 347 | } else if (map instanceof ArrayList){ 348 | ((ArrayList) map).add(valueMap); 349 | } 350 | ((JSObject) value).release(); 351 | continue; 352 | } 353 | 354 | // Primitive types. 355 | if (map instanceof Map) { 356 | ((Map) map).put(key, value); 357 | } else if (map instanceof ArrayList){ 358 | ((ArrayList) map).add(value); 359 | } 360 | } 361 | if (!isArray) { 362 | array.release(); 363 | } 364 | } 365 | 366 | public int getRefCount() { 367 | return refCount; 368 | } 369 | 370 | @Override 371 | public String stringify() { 372 | checkRefCountIsZero(); 373 | return context.stringify(this); 374 | } 375 | 376 | @Override 377 | public boolean isAlive() { 378 | return !isRefCountZero(); 379 | } 380 | 381 | final void checkRefCountIsZero() { 382 | if (isRefCountZero()) { 383 | throw new QuickJSException("The call threw an exception, the reference count of the current object has already reached zero."); 384 | } 385 | } 386 | 387 | public boolean isRefCountZero() { 388 | return refCount == 0; 389 | } 390 | 391 | @Override 392 | public String toString() { 393 | checkRefCountIsZero(); 394 | JSFunction toString = getJSFunction("toString"); 395 | String ret = (String) toString.call(); 396 | toString.release(); 397 | return ret; 398 | } 399 | 400 | @Override 401 | public int hashCode() { 402 | return Arrays.hashCode(new long[]{pointer}); 403 | } 404 | 405 | } 406 | -------------------------------------------------------------------------------- /wrapper-js/extend/libraries/console.js: -------------------------------------------------------------------------------- 1 | // Init format at first. 2 | { 3 | const LINE = "\n" 4 | const TAB = " " 5 | const SPACE = " " 6 | 7 | function format(value, opt) { 8 | const defaultOpt = { 9 | maxStringLength: 10000, 10 | depth: 2, 11 | maxArrayLength: 100, 12 | seen: [], 13 | reduceStringLength: 100 14 | } 15 | if (!opt) { 16 | opt = defaultOpt 17 | } else { 18 | opt = Object.assign(defaultOpt, opt) 19 | } 20 | 21 | return formatValue(value, opt, 0) 22 | } 23 | 24 | function formatValue(value, opt, recurseTimes) { 25 | if (typeof value !== 'object' && typeof value !== 'function') { 26 | return formatPrimitive(value, opt) 27 | } 28 | 29 | if (value === null) { 30 | return 'null' 31 | } 32 | 33 | if (typeof value === 'function') { 34 | return formatFunction(value) 35 | } 36 | 37 | if (typeof value === 'object') { 38 | if (opt.seen.includes(value)) { 39 | let index = 1 40 | if (opt.circular === undefined) { 41 | opt.circular = new Map() 42 | opt.circular.set(value, index) 43 | } else { 44 | index = opt.circular.get(value) 45 | if (index === undefined) { 46 | index = opt.circular.size + 1 47 | opt.circular.set(value, index) 48 | } 49 | } 50 | 51 | return `[Circular *${index}]` 52 | } 53 | 54 | if (opt.depth !== null && ((recurseTimes - 1) === opt.depth)) { 55 | if (value instanceof Array) { 56 | return '[Array]' 57 | } 58 | return '[Object]' 59 | } 60 | 61 | recurseTimes++ 62 | opt.seen.push(value) 63 | const string = formatObject(value, opt, recurseTimes) 64 | opt.seen.pop() 65 | return string 66 | } 67 | } 68 | 69 | function formatObject(value, opt, recurseTimes) { 70 | if (value instanceof RegExp) { 71 | return `${value.toString()}` 72 | } 73 | 74 | if (value instanceof Error) { 75 | return `${value.toString()}` 76 | } 77 | 78 | if (value instanceof Promise) { 79 | // quickjs 环境下通过 native 提供的方式获取 Promise 状态 80 | if (typeof getPromiseState !== "undefined") { 81 | const { result, state} = getPromiseState(value) 82 | if (state === 'fulfilled') { 83 | return `Promise { ${formatValue(result, opt, recurseTimes)} }` 84 | } else if (state === 'rejected'){ 85 | return `Promise { ${formatValue(result, opt, recurseTimes)} }` 86 | } else if (state === 'pending'){ 87 | return `Promise { }` 88 | } 89 | } else { 90 | return `Promise {${formatValue(value, opt, recurseTimes)}}` 91 | } 92 | } 93 | 94 | if (value instanceof Array) { 95 | return formatArray(value, opt, recurseTimes) 96 | } 97 | 98 | if (value instanceof Float64Array) { 99 | return `Float64Array(1) [ ${value} ]` 100 | } 101 | 102 | if (value instanceof BigInt64Array) { 103 | return `BigInt64Array(1) [ ${value}n ]` 104 | } 105 | 106 | if (value instanceof Map) { 107 | return formatMap(value, opt, recurseTimes) 108 | } 109 | 110 | return formatProperty(value, opt, recurseTimes) 111 | } 112 | 113 | function formatProperty(value, opt, recurseTimes) { 114 | let string = '' 115 | string += '{' 116 | const keys = Object.keys(value) 117 | const length = keys.length 118 | for (let i = 0; i < length; i++) { 119 | if (i === 0) { 120 | string += SPACE 121 | } 122 | string += LINE 123 | string += TAB.repeat(recurseTimes) 124 | 125 | const key = keys[i] 126 | string += `${key}: ` 127 | string += formatValue(value[key], opt, recurseTimes) 128 | if (i < length -1) { 129 | string += ',' 130 | } 131 | string += SPACE 132 | } 133 | 134 | string += LINE 135 | string += TAB.repeat(recurseTimes - 1) 136 | string += '}' 137 | 138 | if (string.length < opt.reduceStringLength) { 139 | string = string.replaceAll(LINE, "").replaceAll(TAB, "") 140 | } 141 | 142 | return string 143 | } 144 | 145 | function formatMap(value, opt, recurseTimes) { 146 | let string = `Map(${value.size}) ` 147 | string += '{' 148 | let isEmpty = true 149 | value.forEach((v, k, map) => { 150 | isEmpty = false 151 | string += ` ${format(k, opt, recurseTimes)} => ${format(v, opt, recurseTimes)}` 152 | string += ',' 153 | }) 154 | 155 | if (!isEmpty) { 156 | // 删除最后多余的逗号 157 | string = string.substr(0, string.length -1) + ' ' 158 | } 159 | 160 | string += '}' 161 | return string 162 | } 163 | 164 | function formatArray(value, opt, recurseTimes) { 165 | let string = '[' 166 | value.forEach((item, index, array) => { 167 | if (index === 0) { 168 | string += ' ' 169 | } 170 | string += formatValue(item, opt, recurseTimes) 171 | if (index === opt.maxArrayLength - 1) { 172 | string += `... ${array.length - opt.maxArrayLength} more item${array.length - opt.maxArrayLength > 1 ? 's' : ''}` 173 | } else if (index !== array.length - 1) { 174 | string += ',' 175 | } 176 | string += ' ' 177 | }) 178 | string += ']' 179 | return string 180 | } 181 | 182 | function formatFunction(value) { 183 | let type = 'Function' 184 | 185 | if (value.constructor.name === 'AsyncFunction') { 186 | type = 'AsyncFunction' 187 | } 188 | 189 | if (value.constructor.name === 'GeneratorFunction') { 190 | type = 'GeneratorFunction' 191 | } 192 | 193 | if (value.constructor.name === 'AsyncGeneratorFunction') { 194 | type = 'AsyncGeneratorFunction' 195 | } 196 | 197 | let fn = `${value.name ? `: ${value.name}` : ' (anonymous)'}` 198 | return `[${type + fn}]` 199 | } 200 | 201 | function formatPrimitive(value, opt) { 202 | const type = typeof value 203 | switch (type) { 204 | case "string": 205 | return formatString(value, opt) 206 | case "number": 207 | return Object.is(value, -0) ? '-0' : `${value}` 208 | case "bigint": 209 | return `${String(value)}n` 210 | case "boolean": 211 | return `${value}` 212 | case "undefined": 213 | return "undefined" 214 | case "symbol": 215 | return `${value.toString()}` 216 | default: 217 | return value.toString 218 | } 219 | } 220 | 221 | function formatString(value, opt) { 222 | let trailer = '' 223 | if (opt.maxStringLength && value.length > opt.maxStringLength) { 224 | const remaining = value.length - opt.maxStringLength 225 | value = value.slice(0, opt.maxStringLength) 226 | trailer = `... ${remaining} more character${remaining > 1 ? 's' : ''}` 227 | } 228 | 229 | return `'${value}'${trailer}` 230 | } 231 | 232 | globalThis.format = format 233 | } 234 | 235 | // Then console init. 236 | { 237 | globalThis.console = { 238 | stdout: function (level, msg) { 239 | throw new Error("When invoke console stuff, you should be set a stdout of platform to console.stdout.") 240 | }, 241 | log: function (...args) { 242 | this.print("log", ...args) 243 | }, 244 | debug: function(...args) { 245 | this.print("debug", ...args) 246 | }, 247 | info: function (...args) { 248 | this.print("info", ...args) 249 | }, 250 | warn: function (...args) { 251 | this.print("warn", ...args) 252 | }, 253 | error: function (...args) { 254 | this.print("error", ...args) 255 | }, 256 | print: function (level, ...args) { 257 | let msg = '' 258 | args.forEach((value, index) => { 259 | if (index > 0) { 260 | msg += ", " 261 | } 262 | 263 | msg += globalThis.format(value) 264 | }) 265 | 266 | this.stdout(level, msg) 267 | } 268 | } 269 | } -------------------------------------------------------------------------------- /wrapper-js/extend/libraries/date-polyfill.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | const _Date = Date; 3 | // use _Date avoid recursion in _parse. 4 | const _parse = (date) => { 5 | if (date === null) { 6 | // null is invalid 7 | return new _Date(NaN); 8 | } 9 | if (date === undefined) { 10 | // today 11 | return new _Date(); 12 | } 13 | if (date instanceof Date) { 14 | return new _Date(date); 15 | } 16 | 17 | if (typeof date === 'string' && !/Z$/i.test(date)) { 18 | // YYYY-MM-DD HH:mm:ss.sssZ 19 | const d = date.match(/^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[Tt\s]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?[.:]?(\d+)?$/); 20 | if (d) { 21 | let YYYY = d[1]; 22 | let MM = d[2] - 1 || 0; 23 | let DD = d[3] || 1; 24 | 25 | const HH = d[4] || 0; 26 | const mm = d[5] || 0; 27 | const ss = d[6] || 0; 28 | const sssZ = (d[7] || '0').substring(0, 3); 29 | 30 | // Consider that only date strings (such as "1970-01-01") will be processed as UTC instead of local time. 31 | let utc = (d[4] === undefined) && (d[5] === undefined) && (d[6] === undefined) && (d[7] === undefined); 32 | if (utc) { 33 | return new Date(Date.UTC(YYYY, MM, DD, HH, mm, ss, sssZ)); 34 | } 35 | return new Date(YYYY, MM, DD, HH, mm, ss, sssZ); 36 | } 37 | } 38 | 39 | // everything else 40 | return new _Date(date); 41 | }; 42 | 43 | const handler = { 44 | construct: function (target, args) { 45 | if (args.length === 1 && typeof args[0] === 'string') { 46 | return _parse(args[0]); 47 | } 48 | 49 | return new target(...args); 50 | }, 51 | get(target, prop) { 52 | if (typeof target[prop] === 'function' && target[prop].name === 'parse') { 53 | return new Proxy(target[prop], { 54 | apply: (target, thisArg, argumentsList) => { 55 | if (argumentsList.length === 1 && typeof argumentsList[0] === 'string') { 56 | return _parse(argumentsList[0]).getTime(); 57 | } 58 | 59 | return Reflect.apply(target, thisArg, argumentsList); 60 | } 61 | }); 62 | } else { 63 | return Reflect.get(target, prop); 64 | } 65 | } 66 | }; 67 | 68 | Date = new Proxy(Date, handler); 69 | })(); -------------------------------------------------------------------------------- /wrapper-js/src/index.mjs: -------------------------------------------------------------------------------- 1 | import {existDir, loadFile, removeFile, writeToFile} from "./utils/max-node-fs.mjs"; 2 | 3 | const LINE = "\n" 4 | const TAB = " " 5 | 6 | const DIR_EXTEND_LIBRARIES = "./wrapper-js/extend/libraries/" 7 | const DIR_CPP = "./native/cpp/" 8 | const FILE_H = DIR_CPP + "quickjs_extend_libraries.h" 9 | 10 | if (existDir(FILE_H)) { 11 | removeFile(FILE_H) 12 | } 13 | 14 | (function init() { 15 | let code = "" 16 | 17 | // 以下开始写入扩展库代码 18 | 19 | code += "#ifndef QUICKJS_EXTEND_LIBRARIES" + LINE 20 | code += "#define QUICKJS_EXTEND_LIBRARIES" + LINE 21 | code += LINE 22 | 23 | // include 引用文件 24 | code += `#include ` + LINE 25 | code += `#include ` + LINE 26 | code += `#include "../quickjs/quickjs.h"` + LINE 27 | code += LINE 28 | 29 | // 写入 date-polyfill.js 30 | const datePVarName = "DATE_POLYFILL" 31 | const datePCode = loadFile(DIR_EXTEND_LIBRARIES + "date-polyfill.js") 32 | code += `const char *${datePVarName} = R\"lit(` 33 | code += datePCode 34 | code += ")lit\";" 35 | code += LINE 36 | 37 | code += LINE 38 | 39 | // 写入 console.js 40 | const consoleVarName = "CONSOLE" 41 | const consoleCode = loadFile(DIR_EXTEND_LIBRARIES + "console.js") 42 | code += `const char *${consoleVarName} = R\"lit(` 43 | code += consoleCode 44 | code += ")lit\";" 45 | code += LINE 46 | 47 | code += LINE.repeat(2) 48 | 49 | // 定义入口方法 50 | code += "static inline void loadExtendLibraries(JSContext *ctx) {" 51 | code += LINE 52 | 53 | code += TAB 54 | code += `JS_FreeValue(ctx, JS_Eval(ctx, ${datePVarName}, strlen(${datePVarName}), "date-polyfill.js", JS_EVAL_TYPE_GLOBAL));` 55 | code += LINE 56 | 57 | code += TAB 58 | code += `JS_FreeValue(ctx, JS_Eval(ctx, ${consoleVarName}, strlen(${consoleVarName}), "console.js", JS_EVAL_TYPE_GLOBAL));` 59 | code += LINE 60 | 61 | // 入口方法结尾 62 | code += "}" 63 | 64 | code += LINE.repeat(2) 65 | code += "#endif //QUICKJS_EXTEND_LIBRARIES" 66 | 67 | writeToFile(FILE_H, code) 68 | })() 69 | -------------------------------------------------------------------------------- /wrapper-js/src/utils/max-node-fs.mjs: -------------------------------------------------------------------------------- 1 | import * as fs from 'node:fs'; 2 | import * as path from 'node:path'; 3 | import { exec as _exec } from 'node:child_process'; 4 | import * as util from "node:util" 5 | import * as process from "node:process" 6 | 7 | export function readdir(path) { 8 | return fs.readdirSync(path) 9 | } 10 | 11 | export function loadFile(path) { 12 | return fs.readFileSync(path, "utf-8") 13 | } 14 | 15 | export function mkdir(path) { 16 | fs.mkdirSync(path); 17 | } 18 | 19 | export function writeToFile(path, source) { 20 | fs.writeFileSync(path, source); 21 | } 22 | 23 | export function removeFile(path) { 24 | fs.unlinkSync(path); 25 | } 26 | 27 | export function removeDir(dir) { 28 | let files = fs.readdirSync(dir) 29 | for (let i = 0; i < files.length; i++) { 30 | let newPath = path.join(dir, files[i]); 31 | let stat = fs.statSync(newPath) 32 | if (stat.isDirectory()) { 33 | //如果是文件夹就递归下去 34 | removeDir(newPath); 35 | } else { 36 | //删除文件 37 | fs.unlinkSync(newPath); 38 | } 39 | } 40 | fs.rmdirSync(dir)//如果文件夹是空的,就将自己删除掉 41 | } 42 | 43 | export function existDir(dir) { 44 | return fs.existsSync(dir) 45 | } 46 | 47 | export function exec(command, callback) { 48 | _exec(command, callback) 49 | } 50 | 51 | export function utilFormat(...args) { 52 | return util.format(...args) 53 | } 54 | 55 | export function printWithUpdate(text) { 56 | process.stdout.clearLine() 57 | process.stdout.cursorTo(0) 58 | process.stdout.write(text); 59 | } 60 | 61 | export function cwd() { 62 | return process.cwd() 63 | } 64 | -------------------------------------------------------------------------------- /wrapper-js/test/console-test.js: -------------------------------------------------------------------------------- 1 | globalThis.console.stdout = (level, msg) => globalThis.print(msg) 2 | 3 | const assert = { 4 | strictEqual: function (actual, expected) { 5 | if (arguments.length < 2) { 6 | throw new Error('error missing args'); 7 | } 8 | 9 | if (!Object.is(actual, expected)) { 10 | throw new Error(`${actual} is not equal ${expected}`) 11 | } 12 | } 13 | } 14 | 15 | const assertTrue = (actual) => { 16 | if (!actual) { 17 | throw new Error(`${actual} is not true.`) 18 | } 19 | } 20 | 21 | // Simple test 22 | assert.strictEqual(format(1), '1'); 23 | assert.strictEqual(format(false), 'false'); 24 | assert.strictEqual(format('hello'), "'hello'"); 25 | 26 | // Function test 27 | assert.strictEqual(format(function abc() {}), '[Function: abc]'); 28 | assert.strictEqual(format(async function() {}), '[AsyncFunction (anonymous)]'); 29 | assert.strictEqual(format(async () => {}), '[AsyncFunction (anonymous)]'); 30 | // Special function inspection. 31 | { 32 | const fn = (() => function*() {})(); 33 | assert.strictEqual( 34 | format(fn), 35 | '[GeneratorFunction (anonymous)]' 36 | ); 37 | assert.strictEqual( 38 | format(async function* abc() {}), 39 | '[AsyncGeneratorFunction: abc]' 40 | ); 41 | Object.defineProperty(fn, 'name', { value: 5, configurable: true }); 42 | assert.strictEqual( 43 | format(fn), 44 | '[GeneratorFunction: 5]' 45 | ); 46 | } 47 | 48 | assert.strictEqual(format(undefined), "undefined"); 49 | assert.strictEqual(format(null), "null"); 50 | assert.strictEqual(format(/foo(bar\n)?/gi), '/foo(bar\\n)?/gi'); 51 | assert.strictEqual(format([]), '[]'); 52 | assert.strictEqual(format([1, 2]), '[ 1, 2 ]'); 53 | assert.strictEqual(format([1, [2, 3]]), '[ 1, [ 2, 3 ] ]'); 54 | assert.strictEqual(format({}), '{}'); 55 | assert.strictEqual(format({ a: 1 }), '{ a: 1 }'); 56 | assert.strictEqual(format({ a: function() {} }), '{ a: [Function: a] }'); 57 | assert.strictEqual(format({ a: () => {} }), '{ a: [Function: a] }'); 58 | // eslint-disable-next-line func-name-matching 59 | assert.strictEqual(format({ a: async function abc() {} }), 60 | '{ a: [AsyncFunction: abc] }'); 61 | assert.strictEqual(format({ a: async () => {} }), 62 | '{ a: [AsyncFunction: a] }'); 63 | assert.strictEqual(format({ a: function*() {} }), 64 | '{ a: [GeneratorFunction: a] }'); 65 | assert.strictEqual(format({ a: 1, b: 2 }), '{ a: 1, b: 2 }'); 66 | assert.strictEqual(format({ 'a': {} }), '{ a: {} }'); 67 | assert.strictEqual(format({ 'a': { 'b': 2 } }), '{ a: { b: 2 } }'); 68 | assert.strictEqual(format({ 'a': { 'b': { 'c': { 'd': 2 } } } }), 69 | '{ a: { b: { c: [Object] } } }'); 70 | assert.strictEqual( 71 | format({ 'a': { 'b': { 'c': { 'd': 2 } } } }, {depth: null}), 72 | '{ a: { b: { c: { d: 2 } } } }'); 73 | assert.strictEqual(format([1, 2, 3]), '[ 1, 2, 3 ]'); 74 | assert.strictEqual(format({ 'a': { 'b': { 'c': 2 } } }, { depth: 0 }), 75 | '{ a: [Object] }'); 76 | assert.strictEqual(format({ 'a': { 'b': { 'c': 2 } } }, { depth: 1 }), 77 | '{ a: { b: [Object] } }'); 78 | assert.strictEqual(format({ 'a': { 'b': ['c'] } }, { depth: 1 }), 79 | '{ a: { b: [Array] } }'); 80 | 81 | // Test Map. 82 | { 83 | assert.strictEqual(format(new Map()), 'Map(0) {}'); 84 | assert.strictEqual(format(new Map([[1, 'a'], [2, 'b'], [3, 'c']])), 85 | "Map(3) { 1 => 'a', 2 => 'b', 3 => 'c' }"); 86 | const map = new Map([['foo', null]]); 87 | assert.strictEqual(format(map), 88 | "Map(1) { 'foo' => null }"); 89 | } 90 | 91 | // Test circular Map. 92 | { 93 | const map = new Map(); 94 | map.set(map, 'map'); 95 | assert.strictEqual( 96 | format(map), 97 | "Map(1) { [Circular *1] => 'map' }" 98 | ); 99 | map.set(map, map); 100 | assert.strictEqual( 101 | format(map), 102 | 'Map(1) { [Circular *1] => [Circular *1] }' 103 | ); 104 | map.delete(map); 105 | map.set('map', map); 106 | assert.strictEqual( 107 | format(map), 108 | "Map(1) { 'map' => [Circular *1] }" 109 | ); 110 | } 111 | 112 | // Test multiple circular references. 113 | { 114 | const obj = {}; 115 | obj.a = [obj]; 116 | obj.b = {}; 117 | obj.b.inner = obj.b; 118 | obj.b.obj = obj; 119 | 120 | assert.strictEqual( 121 | format(obj), 122 | '{' + 123 | ' a: [ [Circular *1] ],' + 124 | ' b: { inner: [Circular *2], obj: [Circular *1] } ' + 125 | '}' 126 | ); 127 | } 128 | 129 | // Test Promise. 130 | { 131 | // quickjs 环境下通过 native 提供的方式获取 Promise 状态 132 | if (typeof getPromiseState !== "undefined") { 133 | const resolved = Promise.resolve(3); 134 | assert.strictEqual(format(resolved), 'Promise { 3 }'); 135 | 136 | const rejected = Promise.reject(3); 137 | assert.strictEqual(format(rejected), 'Promise { 3 }'); 138 | // Squelch UnhandledPromiseRejection. 139 | rejected.catch(() => {}); 140 | 141 | const pending = new Promise(() => {}); 142 | assert.strictEqual(format(pending), 'Promise { }'); 143 | 144 | const promiseWithProperty = Promise.resolve('foo'); 145 | assert.strictEqual(format(promiseWithProperty), 146 | "Promise { 'foo' }"); 147 | } 148 | } 149 | 150 | // Truncate output for Primitives with 1 character left 151 | { 152 | assert.strictEqual(format('bl', { maxStringLength: 1 }), 153 | "'b'... 1 more character"); 154 | } 155 | 156 | { 157 | const x = 'a'.repeat(1e6); 158 | assertTrue(format(x).endsWith('... 990000 more characters')); 159 | assert.strictEqual( 160 | format(x, { maxStringLength: 4 }), 161 | "'aaaa'... 999996 more characters" 162 | ); 163 | assertTrue(format(x, { maxStringLength: null }).endsWith(`a'`)); 164 | } 165 | 166 | { 167 | assert.strictEqual( 168 | // eslint-disable-next-line no-loss-of-precision 169 | format(1234567891234567891234), 170 | '1.234567891234568e+21' 171 | ); 172 | assert.strictEqual( 173 | format(123456789.12345678), 174 | '123456789.12345678' 175 | ); 176 | 177 | assert.strictEqual(format(10_000_000), '10000000'); 178 | assert.strictEqual(format(1_000_000), '1000000'); 179 | assert.strictEqual(format(100_000), '100000'); 180 | assert.strictEqual(format(99_999.9), '99999.9'); 181 | assert.strictEqual(format(9_999), '9999'); 182 | assert.strictEqual(format(999), '999'); 183 | assert.strictEqual(format(NaN), 'NaN'); 184 | assert.strictEqual(format(Infinity), 'Infinity'); 185 | assert.strictEqual(format(-Infinity), '-Infinity'); 186 | 187 | assert.strictEqual( 188 | format(new Float64Array([100_000_000])), 189 | 'Float64Array(1) [ 100000000 ]' 190 | ); 191 | assert.strictEqual( 192 | format(new BigInt64Array([9_100_000_100n])), 193 | 'BigInt64Array(1) [ 9100000100n ]' 194 | ); 195 | 196 | assert.strictEqual( 197 | format(123456789), 198 | '123456789' 199 | ); 200 | assert.strictEqual( 201 | format(123456789n), 202 | '123456789n' 203 | ); 204 | 205 | assert.strictEqual( 206 | format(123456789.12345678), 207 | '123456789.12345678' 208 | ); 209 | 210 | assert.strictEqual( 211 | format(-123456789.12345678), 212 | '-123456789.12345678' 213 | ); 214 | } 215 | 216 | // Test es6 Symbol. 217 | { 218 | assert.strictEqual(format(Symbol()), 'Symbol()'); 219 | assert.strictEqual(format(Symbol(123)), 'Symbol(123)'); 220 | assert.strictEqual(format(Symbol('hi')), 'Symbol(hi)'); 221 | assert.strictEqual(format([Symbol()]), '[ Symbol() ]'); 222 | assert.strictEqual(format({ foo: Symbol() }), '{ foo: Symbol() }'); 223 | } 224 | 225 | // Test Error. 226 | { 227 | assert.strictEqual(format(new Error('123')), 'Error: 123') 228 | } 229 | 230 | console.log("✅ 测试通过") 231 | --------------------------------------------------------------------------------