From cb3ce477e2a4e43de214f994c1985b55664212e1 Mon Sep 17 00:00:00 2001 From: Alexey Andreev Date: Wed, 9 Oct 2024 20:50:07 +0200 Subject: [PATCH] wasm gc: preprocess JS runtime with uglifyjs, add modular runtime --- .gitignore | 4 +- core/build.gradle.kts | 56 ++ core/package-lock.json | 25 + core/package.json | 8 + .../main/js/wasm-gc-runtime/module-wrapper.js | 18 + core/src/main/js/wasm-gc-runtime/runtime.js | 609 ++++++++++++++++++ .../main/js/wasm-gc-runtime/simple-wrapper.js | 22 + .../org/teavm/backend/wasm/wasm-gc-runtime.js | 609 ------------------ gradle/libs.versions.toml | 3 +- .../java/org/teavm/jso/export/ExportTest.java | 4 +- tools/deobfuscator-js/build.gradle.kts | 2 +- .../gradle/tasks/CopyWasmGCRuntimeTask.java | 2 +- 12 files changed, 746 insertions(+), 616 deletions(-) create mode 100644 core/package-lock.json create mode 100644 core/package.json create mode 100644 core/src/main/js/wasm-gc-runtime/module-wrapper.js create mode 100644 core/src/main/js/wasm-gc-runtime/runtime.js create mode 100644 core/src/main/js/wasm-gc-runtime/simple-wrapper.js delete mode 100644 core/src/main/resources/org/teavm/backend/wasm/wasm-gc-runtime.js diff --git a/.gitignore b/.gitignore index 4ee3b5065..187c785ba 100644 --- a/.gitignore +++ b/.gitignore @@ -4,13 +4,13 @@ build /build-cache /deploy-with-env.sh /release-with-env.sh -/.gradle -/build-logic/.gradle +.gradle /relocated /samples/.gradle local.properties teavm-local.properties .kotlin +node_modules # KDE .directory diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 27e7c2553..1e783a3b1 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -1,3 +1,5 @@ +import com.github.gradle.node.npm.task.NpmTask + /* * Copyright 2023 Alexey Andreev. * @@ -17,10 +19,15 @@ plugins { `java-library` `teavm-publish` + alias(libs.plugins.nodejs) } description = "Compiler, backends and runtime" +node { + download = true +} + dependencies { api(project(":interop:core")) api(project(":metaprogramming:api")) @@ -35,6 +42,55 @@ dependencies { testImplementation(libs.junit) } +val jsOutputDir = layout.buildDirectory.dir("generated/js") +val jsOutputPackageDir = jsOutputDir.map { it.dir("org/teavm/backend/wasm") } +val jsInputDir = layout.projectDirectory.dir("src/main/js/wasm-gc-runtime") +val jsInput = jsInputDir.file("runtime.js") + +fun registerRuntimeTasks(taskName: String, wrapperType: String, outputName: String) { + val generateTask by tasks.register("generate${taskName}Runtime") { + val wrapperFile = jsInputDir.file(wrapperType) + val runtimeFile = jsInput + val outputFile = jsOutputPackageDir.map { it.file("$outputName.js") } + inputs.files(wrapperFile, runtimeFile) + outputs.file(outputFile) + doLast { + val wrapper = wrapperFile.asFile.readText() + var runtime = runtimeFile.asFile.readText() + val startText = "// !BEGINNING!\n" + val startIndex = runtime.indexOf(startText) + if (startIndex >= 0) { + runtime = runtime.substring(startIndex + startText.length) + } + outputFile.get().asFile.writeText(wrapper.replace("include();", runtime)) + } + } + + val optimizeTask = tasks.register("optimize${taskName}Runtime") { + val inputFiles = generateTask.outputs.files + val outputFile = jsOutputPackageDir.map { it.file("$outputName.min.js") } + inputs.files(inputFiles) + outputs.file(outputFile) + npmCommand.addAll("run", "uglify") + args.addAll(provider { + listOf( + "--", + "-m", "--module", "--toplevel", + inputFiles.singleFile.absolutePath, + "-o", outputFile.get().asFile.absolutePath + ) + }) + } + + sourceSets.main.configure { + output.dir(mapOf("builtBy" to generateTask), jsOutputDir) + output.dir(mapOf("builtBy" to optimizeTask), jsOutputDir) + } +} + +registerRuntimeTasks("Simple", "simple-wrapper.js", "wasm-gc-runtime") +registerRuntimeTasks("Module", "module-wrapper.js", "wasm-gc-module-runtime") + teavmPublish { artifactId = "teavm-core" } \ No newline at end of file diff --git a/core/package-lock.json b/core/package-lock.json new file mode 100644 index 000000000..bcdeac3d7 --- /dev/null +++ b/core/package-lock.json @@ -0,0 +1,25 @@ +{ + "name": "core", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "uglify-js": "3.19.3" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + } + } +} diff --git a/core/package.json b/core/package.json new file mode 100644 index 000000000..aaf391dc7 --- /dev/null +++ b/core/package.json @@ -0,0 +1,8 @@ +{ + "scripts": { + "uglify": "uglifyjs" + }, + "devDependencies": { + "uglify-js": "3.19.3" + } +} diff --git a/core/src/main/js/wasm-gc-runtime/module-wrapper.js b/core/src/main/js/wasm-gc-runtime/module-wrapper.js new file mode 100644 index 000000000..4abbfc5c7 --- /dev/null +++ b/core/src/main/js/wasm-gc-runtime/module-wrapper.js @@ -0,0 +1,18 @@ +/* + * Copyright 2024 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +include(); +export { load, defaults }; \ No newline at end of file diff --git a/core/src/main/js/wasm-gc-runtime/runtime.js b/core/src/main/js/wasm-gc-runtime/runtime.js new file mode 100644 index 000000000..88dd98f21 --- /dev/null +++ b/core/src/main/js/wasm-gc-runtime/runtime.js @@ -0,0 +1,609 @@ +/* + * Copyright 2024 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// !BEGINNING! + +let globalsCache = new Map(); +let stackDeobfuscator = null; +let chromeExceptionRegex = / *at .+\.wasm:wasm-function\[[0-9]+]:0x([0-9a-f]+).*/; +let getGlobalName = function(name) { + let result = globalsCache.get(name); + if (typeof result === "undefined") { + result = new Function("return " + name + ";"); + globalsCache.set(name, result); + } + return result(); +} +let setGlobalName = function(name, value) { + new Function("value", name + " = value;")(value); +} + +function defaults(imports) { + let context = { + exports: null + }; + dateImports(imports); + consoleImports(imports, context); + coreImports(imports, context); + jsoImports(imports, context); + imports.teavmMath = Math; + return { + supplyExports(exports) { + context.exports = exports + } + } +} + +let javaExceptionSymbol = Symbol("javaException"); +class JavaError extends Error { + constructor(javaException) { + super(); + this[javaExceptionSymbol] = javaException; + } + get message() { + let exceptionMessage = exports["teavm.exceptionMessage"]; + if (typeof exceptionMessage === "function") { + let message = exceptionMessage(this[javaExceptionSymbol]); + if (message != null) { + return message; + } + } + return "(could not fetch message)"; + } +} + +function dateImports(imports) { + imports.teavmDate = { + currentTimeMillis: () => new Date().getTime(), + dateToString: timestamp => new Date(timestamp).toString(), + getYear: timestamp => new Date(timestamp).getFullYear(), + setYear(timestamp, year) { + let date = new Date(timestamp); + date.setFullYear(year); + return date.getTime(); + }, + getMonth: timestamp =>new Date(timestamp).getMonth(), + setMonth(timestamp, month) { + let date = new Date(timestamp); + date.setMonth(month); + return date.getTime(); + }, + getDate: timestamp =>new Date(timestamp).getDate(), + setDate(timestamp, value) { + let date = new Date(timestamp); + date.setDate(value); + return date.getTime(); + }, + create: (year, month, date, hrs, min, sec) => new Date(year, month, date, hrs, min, sec).getTime(), + createFromUTC: (year, month, date, hrs, min, sec) => Date.UTC(year, month, date, hrs, min, sec) + }; +} + +function consoleImports(imports) { + let stderr = ""; + let stdout = ""; + imports.teavmConsole = { + putcharStderr(c) { + if (c === 10) { + console.error(stderr); + stderr = ""; + } else { + stderr += String.fromCharCode(c); + } + }, + putcharStdout(c) { + if (c === 10) { + console.log(stdout); + stdout = ""; + } else { + stdout += String.fromCharCode(c); + } + }, + }; +} + +function coreImports(imports, context) { + let finalizationRegistry = new FinalizationRegistry(heldValue => { + let report = context.exports["teavm.reportGarbageCollectedValue"]; + if (typeof report === "function") { + report(heldValue) + } + }); + let stringFinalizationRegistry = new FinalizationRegistry(heldValue => { + let report = context.exports["teavm.reportGarbageCollectedString"]; + if (typeof report === "function") { + report(heldValue); + } + }); + imports.teavm = { + createWeakRef(value, heldValue) { + let weakRef = new WeakRef(value); + if (heldValue !== null) { + finalizationRegistry.register(value, heldValue) + } + return weakRef; + }, + deref: weakRef => weakRef.deref(), + createStringWeakRef(value, heldValue) { + let weakRef = new WeakRef(value); + stringFinalizationRegistry.register(value, heldValue) + return weakRef; + }, + stringDeref: weakRef => weakRef.deref(), + currentStackTrace() { + if (stackDeobfuscator) { + return; + } + let reportCallFrame = context.exports["teavm.reportCallFrame"]; + if (typeof reportCallFrame !== "function") { + return; + } + let stack = new Error().stack; + for (let line in stack.split("\n")) { + let match = chromeExceptionRegex.exec(line); + if (match !== null) { + let address = parseInt(match.groups[1], 16); + let frames = stackDeobfuscator(address); + for (let frame of frames) { + let line = frame.line; + reportCallFrame(file, method, cls, line); + } + } + } + } + }; +} + +function jsoImports(imports, context) { + let javaObjectSymbol = Symbol("javaObject"); + let functionsSymbol = Symbol("functions"); + let functionOriginSymbol = Symbol("functionOrigin"); + let wrapperCallMarkerSymbol = Symbol("wrapperCallMarker"); + + let jsWrappers = new WeakMap(); + let javaWrappers = new WeakMap(); + let primitiveWrappers = new Map(); + let primitiveFinalization = new FinalizationRegistry(token => primitiveFinalization.delete(token)); + let hashCodes = new WeakMap(); + let javaExceptionWrappers = new WeakMap(); + let lastHashCode = 2463534242; + let nextHashCode = () => { + let x = lastHashCode; + x ^= x << 13; + x ^= x >>> 17; + x ^= x << 5; + lastHashCode = x; + return x; + } + + function identity(value) { + return value; + } + function sanitizeName(str) { + let result = ""; + let firstChar = str.charAt(0); + result += isIdentifierStart(firstChar) ? firstChar : '_'; + for (let i = 1; i < str.length; ++i) { + let c = str.charAt(i) + result += isIdentifierPart(c) ? c : '_'; + } + return result; + } + function isIdentifierStart(s) { + return s >= 'A' && s <= 'Z' || s >= 'a' && s <= 'z' || s === '_' || s === '$'; + } + function isIdentifierPart(s) { + return isIdentifierStart(s) || s >= '0' && s <= '9'; + } + function setProperty(obj, prop, value) { + if (obj === null) { + setGlobalName(prop, value); + } else { + obj[prop] = value; + } + } + function javaExceptionToJs(e) { + if (e instanceof WebAssembly.Exception) { + let tag = context.exports["teavm.javaException"]; + if (e.is(tag)) { + let javaException = e.getArg(tag, 0); + let extracted = extractException(javaException); + if (extracted !== null) { + return extracted; + } + let wrapperRef = javaExceptionWrappers.get(javaException); + if (typeof wrapperRef != "undefined") { + let wrapper = wrapperRef.deref(); + if (typeof wrapper !== "undefined") { + return wrapper; + } + } + let wrapper = new JavaError(javaException); + javaExceptionWrappers.set(javaException, new WeakRef(wrapper)); + return wrapper; + } + } + return e; + } + function jsExceptionAsJava(e) { + if (javaExceptionSymbol in e) { + return e[javaExceptionSymbol]; + } else { + return context.exports["teavm.js.wrapException"](e); + } + } + function rethrowJsAsJava(e) { + context.exports["teavm.js.throwException"](jsExceptionAsJava(e)); + } + function extractException(e) { + return context.exports["teavm.js.extractException"](e); + } + function rethrowJavaAsJs(e) { + throw javaExceptionToJs(e); + } + function getProperty(obj, prop) { + try { + return obj !== null ? obj[prop] : getGlobalName(prop) + } catch (e) { + rethrowJsAsJava(e); + } + } + function defineFunction(fn) { + let params = []; + for (let i = 0; i < fn.length; ++i) { + params.push("p" + i); + } + let paramsAsString = params.length === 0 ? "" : params.join(", "); + return new Function("rethrowJavaAsJs", "fn", + `return function(${paramsAsString}) {\n` + + ` try {\n` + + ` return fn(${paramsAsString});\n` + + ` } catch (e) {\n` + + ` rethrowJavaAsJs(e);\n` + + ` }\n` + + `};` + )(rethrowJavaAsJs, fn); + } + function renameConstructor(name, c) { + return new Function( + "constructor", + `return function ${name}(marker, javaObject) {\n` + + ` return constructor.call(this, marker, javaObject);\n` + + `}\n` + )(c); + } + imports.teavmJso = { + emptyString: () => "", + stringFromCharCode: code => String.fromCharCode(code), + concatStrings: (a, b) => a + b, + stringLength: s => s.length, + charAt: (s, index) => s.charCodeAt(index), + emptyArray: () => [], + appendToArray: (array, e) => array.push(e), + unwrapBoolean: value => value ? 1 : 0, + wrapBoolean: value => !!value, + getProperty: getProperty, + getPropertyPure: getProperty, + setProperty: setProperty, + setPropertyPure: setProperty, + global(name) { + try { + return getGlobalName(name); + } catch (e) { + rethrowJsAsJava(e); + } + }, + createClass(name, parent, constructor) { + name = sanitizeName(name || "JavaObject"); + let action; + if (parent === null) { + action = function (javaObject) { + this[javaObjectSymbol] = javaObject; + this[functionsSymbol] = null; + }; + } else { + action = function (javaObject) { + parent.call(this, javaObject); + }; + fn.prototype = Object.create(parent); + fn.prototype.constructor = parent; + } + let fn = renameConstructor(name, function (marker, javaObject) { + if (marker === wrapperCallMarkerSymbol) { + action.call(this, javaObject); + } else if (constructor === null) { + throw new Error("This class can't be instantiated directly"); + } else { + try { + return constructor.apply(null, arguments); + } catch (e) { + rethrowJavaAsJs(e); + } + } + }); + fn.prototype = Object.create(parent || Object.prototype); + fn.prototype.constructor = fn; + let boundFn = renameConstructor(name, function(javaObject) { + return fn.call(this, wrapperCallMarkerSymbol, javaObject); + }); + boundFn[wrapperCallMarkerSymbol] = fn; + boundFn.prototype = fn.prototype; + return boundFn; + }, + exportClass(cls) { + return cls[wrapperCallMarkerSymbol]; + }, + defineMethod(cls, name, fn) { + let params = []; + for (let i = 1; i < fn.length; ++i) { + params.push("p" + i); + } + let paramsAsString = params.length === 0 ? "" : params.join(", "); + cls.prototype[name] = new Function("rethrowJavaAsJs", "fn", + `return function(${paramsAsString}) {\n` + + ` try {\n` + + ` return fn(${['this', params].join(", ")});\n` + + ` } catch (e) {\n` + + ` rethrowJavaAsJs(e);\n` + + ` }\n` + + `};` + )(rethrowJavaAsJs, fn); + }, + defineStaticMethod(cls, name, fn) { + cls[name] = defineFunction(fn); + }, + defineFunction: defineFunction, + defineProperty(cls, name, getFn, setFn) { + let descriptor = { + get() { + try { + return getFn(this); + } catch (e) { + rethrowJavaAsJs(e); + } + } + }; + if (setFn !== null) { + descriptor.set = function(value) { + try { + setFn(this, value); + } catch (e) { + rethrowJavaAsJs(e); + } + } + } + Object.defineProperty(cls.prototype, name, descriptor); + }, + defineStaticProperty(cls, name, getFn, setFn) { + let descriptor = { + get() { + try { + return getFn(); + } catch (e) { + rethrowJavaAsJs(e); + } + } + }; + if (setFn !== null) { + descriptor.set = function(value) { + try { + setFn(value); + } catch (e) { + rethrowJavaAsJs(e); + } + } + } + Object.defineProperty(cls, name, descriptor); + }, + javaObjectToJS(instance, cls) { + let existing = jsWrappers.get(instance); + if (typeof existing != "undefined") { + let result = existing.deref(); + if (typeof result !== "undefined") { + return result; + } + } + let obj = new cls(instance); + jsWrappers.set(instance, new WeakRef(obj)); + return obj; + }, + unwrapJavaObject(instance) { + return instance[javaObjectSymbol]; + }, + asFunction(instance, propertyName) { + let functions = instance[functionsSymbol]; + if (functions === null) { + functions = Object.create(null); + instance[functionsSymbol] = functions; + } + let result = functions[propertyName]; + if (typeof result !== 'function') { + result = function() { + return instance[propertyName].apply(instance, arguments); + } + result[functionOriginSymbol] = instance; + functions[propertyName] = result; + } + return result; + }, + functionAsObject(fn, property) { + let origin = fn[functionOriginSymbol]; + if (typeof origin !== 'undefined') { + let functions = origin[functionsSymbol]; + if (functions !== void 0 && functions[property] === fn) { + return origin; + } + } + return { + [property]: function(...args) { + try { + return fn(...args); + } catch (e) { + rethrowJavaAsJs(e); + } + } + }; + }, + wrapObject(obj) { + if (obj === null) { + return null; + } + if (typeof obj === "object" || typeof obj === "function" || typeof "obj" === "symbol") { + let result = obj[javaObjectSymbol]; + if (typeof result === "object") { + return result; + } + result = javaWrappers.get(obj); + if (result !== void 0) { + result = result.deref(); + if (result !== void 0) { + return result; + } + } + result = context.exports["teavm.jso.createWrapper"](obj); + javaWrappers.set(obj, new WeakRef(result)); + return result; + } else { + let result = primitiveWrappers.get(obj); + if (result !== void 0) { + result = result.deref(); + if (result !== void 0) { + return result; + } + } + result = context.exports["teavm.jso.createWrapper"](obj); + primitiveWrappers.set(obj, new WeakRef(result)); + primitiveFinalization.register(result, obj); + return result; + } + }, + isPrimitive: (value, type) => typeof value === type, + instanceOf: (value, type) => value instanceof type, + instanceOfOrNull: (value, type) => value === null || value instanceof type, + sameRef: (a, b) => a === b, + hashCode: (obj) => { + if (typeof obj === "object" || typeof obj === "function" || typeof obj === "symbol") { + let code = hashCodes.get(obj); + if (typeof code === "number") { + return code; + } + code = nextHashCode(); + hashCodes.set(obj, code); + return code; + } else if (typeof obj === "number") { + return obj | 0; + } else if (typeof obj === "bigint") { + return BigInt.asIntN(obj, 32); + } else if (typeof obj === "boolean") { + return obj ? 1 : 0; + } else { + return 0; + } + }, + apply: (instance, method, args) => { + try { + if (instance === null) { + let fn = getGlobalName(method); + return fn(...args); + } else { + return instance[method](...args); + } + } catch (e) { + rethrowJsAsJava(e); + } + }, + concatArray: (a, b) => a.concat(b), + getJavaException: e => e[javaExceptionSymbol] + }; + for (let name of ["wrapByte", "wrapShort", "wrapChar", "wrapInt", "wrapFloat", "wrapDouble", "unwrapByte", + "unwrapShort", "unwrapChar", "unwrapInt", "unwrapFloat", "unwrapDouble"]) { + imports.teavmJso[name] = identity; + } + function wrapCallFromJavaToJs(call) { + try { + return call(); + } catch (e) { + rethrowJsAsJava(e); + } + } + let argumentList = []; + for (let i = 0; i < 32; ++i) { + let args = argumentList.length === 0 ? "" : argumentList.join(", "); + let argsAndBody = [...argumentList, "body"].join(", "); + imports.teavmJso["createFunction" + i] = new Function("wrapCallFromJavaToJs", ...argumentList, "body", + `return new Function('wrapCallFromJavaToJs', ${argsAndBody}).bind(this, wrapCallFromJavaToJs);` + ).bind(null, wrapCallFromJavaToJs); + imports.teavmJso["callFunction" + i] = new Function("rethrowJsAsJava", "fn", ...argumentList, + `try {\n` + + ` return fn(${args});\n` + + `} catch (e) {\n` + + ` rethrowJsAsJava(e);\n` + + `}` + ).bind(null, rethrowJsAsJava); + imports.teavmJso["callMethod" + i] = new Function("rethrowJsAsJava", "getGlobalName", "instance", + "method", ...argumentList, + `try {\n`+ + ` return instance !== null\n` + + ` ? instance[method](${args})\n` + + ` : getGlobalName(method)(${args});\n` + + `} catch (e) {\n` + + ` rethrowJsAsJava(e);\n` + + `}` + ).bind(null, rethrowJsAsJava, getGlobalName); + imports.teavmJso["construct" + i] = new Function("rethrowJsAsJava", "constructor", ...argumentList, + `try {\n` + + ` return new constructor(${args});\n` + + `} catch (e) {\n` + + ` rethrowJsAsJava(e);\n` + + `}` + ).bind(null, rethrowJsAsJava); + imports.teavmJso["arrayOf" + i] = new Function(...argumentList, "return [" + args + "]"); + + let param = "p" + (i + 1); + argumentList.push(param); + } +} + +function load(path, options) { + if (!options) { + options = {}; + } + + const importObj = {}; + const defaultsResults = defaults(importObj); + if (typeof options.installImports !== "undefined") { + options.installImports(importObj); + } + + return WebAssembly.instantiateStreaming(fetch(path), importObj) + .then(r => { + defaultsResults.supplyExports(r.instance.exports); + let userExports = {}; + let teavm = { + exports: userExports, + instance: r.instance, + module: r.module + }; + for (let key in r.instance.exports) { + let exportObj = r.instance.exports[key]; + if (exportObj instanceof WebAssembly.Global) { + Object.defineProperty(userExports, key, { + get: () => exportObj.value + }); + } + } + return teavm; + }); +} \ No newline at end of file diff --git a/core/src/main/js/wasm-gc-runtime/simple-wrapper.js b/core/src/main/js/wasm-gc-runtime/simple-wrapper.js new file mode 100644 index 000000000..c3845bf89 --- /dev/null +++ b/core/src/main/js/wasm-gc-runtime/simple-wrapper.js @@ -0,0 +1,22 @@ +/* + * Copyright 2024 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var TeaVM = TeaVM || {}; +TeaVM.wasmGC = TeaVM.wasmGC || (() => { + include(); + return { load, defaults }; +})(); + diff --git a/core/src/main/resources/org/teavm/backend/wasm/wasm-gc-runtime.js b/core/src/main/resources/org/teavm/backend/wasm/wasm-gc-runtime.js deleted file mode 100644 index e6c478c4e..000000000 --- a/core/src/main/resources/org/teavm/backend/wasm/wasm-gc-runtime.js +++ /dev/null @@ -1,609 +0,0 @@ -/* - * Copyright 2024 Alexey Andreev. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -var TeaVM = TeaVM || {}; -if (window && window.TeaVM === undefined) { - window.TeaVM = TeaVM; -} -TeaVM.wasmGC = TeaVM.wasmGC || function() { - let exports; - let globalsCache = new Map(); - let stackDeobfuscator = null; - let chromeExceptionRegex = / *at .+\.wasm:wasm-function\[[0-9]+]:0x([0-9a-f]+).*/; - let getGlobalName = function(name) { - let result = globalsCache.get(name); - if (typeof result === "undefined") { - result = new Function("return " + name + ";"); - globalsCache.set(name, result); - } - return result(); - } - let setGlobalName = function(name, value) { - new Function("value", name + " = value;")(value); - } - - function defaults(imports) { - dateImports(imports); - consoleImports(imports); - coreImports(imports); - jsoImports(imports); - imports.teavmMath = Math; - } - - let javaExceptionSymbol = Symbol("javaException"); - class JavaError extends Error { - constructor(javaException) { - super(); - this[javaExceptionSymbol] = javaException; - } - get message() { - let exceptionMessage = exports["teavm.exceptionMessage"]; - if (typeof exceptionMessage === "function") { - let message = exceptionMessage(this[javaExceptionSymbol]); - if (message != null) { - return message; - } - } - return "(could not fetch message)"; - } - } - - function dateImports(imports) { - imports.teavmDate = { - currentTimeMillis: () => new Date().getTime(), - dateToString: timestamp => new Date(timestamp).toString(), - getYear: timestamp => new Date(timestamp).getFullYear(), - setYear(timestamp, year) { - let date = new Date(timestamp); - date.setFullYear(year); - return date.getTime(); - }, - getMonth: timestamp =>new Date(timestamp).getMonth(), - setMonth(timestamp, month) { - let date = new Date(timestamp); - date.setMonth(month); - return date.getTime(); - }, - getDate: timestamp =>new Date(timestamp).getDate(), - setDate(timestamp, value) { - let date = new Date(timestamp); - date.setDate(value); - return date.getTime(); - }, - create: (year, month, date, hrs, min, sec) => new Date(year, month, date, hrs, min, sec).getTime(), - createFromUTC: (year, month, date, hrs, min, sec) => Date.UTC(year, month, date, hrs, min, sec) - }; - } - - function consoleImports(imports) { - let stderr = ""; - let stdout = ""; - imports.teavmConsole = { - putcharStderr(c) { - if (c === 10) { - console.error(stderr); - stderr = ""; - } else { - stderr += String.fromCharCode(c); - } - }, - putcharStdout(c) { - if (c === 10) { - console.log(stdout); - stdout = ""; - } else { - stdout += String.fromCharCode(c); - } - }, - }; - } - - function coreImports(imports) { - let finalizationRegistry = new FinalizationRegistry(heldValue => { - let report = exports["teavm.reportGarbageCollectedValue"]; - if (typeof report === "function") { - report(heldValue) - } - }); - let stringFinalizationRegistry = new FinalizationRegistry(heldValue => { - let report = exports["teavm.reportGarbageCollectedString"]; - if (typeof report === "function") { - report(heldValue); - } - }); - imports.teavm = { - createWeakRef(value, heldValue) { - let weakRef = new WeakRef(value); - if (heldValue !== null) { - finalizationRegistry.register(value, heldValue) - } - return weakRef; - }, - deref: weakRef => weakRef.deref(), - createStringWeakRef(value, heldValue) { - let weakRef = new WeakRef(value); - stringFinalizationRegistry.register(value, heldValue) - return weakRef; - }, - stringDeref: weakRef => weakRef.deref(), - currentStackTrace() { - if (stackDeobfuscator) { - return; - } - let reportCallFrame = exports["teavm.reportCallFrame"]; - if (typeof reportCallFrame !== "function") { - return; - } - let stack = new Error().stack; - for (let line in stack.split("\n")) { - let match = chromeExceptionRegex.exec(line); - if (match !== null) { - let address = parseInt(match.groups[1], 16); - let frames = stackDeobfuscator(address); - for (let frame of frames) { - let line = frame.line; - reportCallFrame(file, method, cls, line); - } - } - } - } - }; - } - - function jsoImports(imports) { - let javaObjectSymbol = Symbol("javaObject"); - let functionsSymbol = Symbol("functions"); - let functionOriginSymbol = Symbol("functionOrigin"); - let wrapperCallMarkerSymbol = Symbol("wrapperCallMarker"); - - let jsWrappers = new WeakMap(); - let javaWrappers = new WeakMap(); - let primitiveWrappers = new Map(); - let primitiveFinalization = new FinalizationRegistry(token => primitiveFinalization.delete(token)); - let hashCodes = new WeakMap(); - let javaExceptionWrappers = new WeakMap(); - let lastHashCode = 2463534242; - let nextHashCode = () => { - let x = lastHashCode; - x ^= x << 13; - x ^= x >>> 17; - x ^= x << 5; - lastHashCode = x; - return x; - } - - function identity(value) { - return value; - } - function sanitizeName(str) { - let result = ""; - let firstChar = str.charAt(0); - result += isIdentifierStart(firstChar) ? firstChar : '_'; - for (let i = 1; i < str.length; ++i) { - let c = str.charAt(i) - result += isIdentifierPart(c) ? c : '_'; - } - return result; - } - function isIdentifierStart(s) { - return s >= 'A' && s <= 'Z' || s >= 'a' && s <= 'z' || s === '_' || s === '$'; - } - function isIdentifierPart(s) { - return isIdentifierStart(s) || s >= '0' && s <= '9'; - } - function setProperty(obj, prop, value) { - if (obj === null) { - setGlobalName(prop, value); - } else { - obj[prop] = value; - } - } - function javaExceptionToJs(e) { - if (e instanceof WebAssembly.Exception) { - let tag = exports["teavm.javaException"]; - if (e.is(tag)) { - let javaException = e.getArg(tag, 0); - let extracted = extractException(javaException); - if (extracted !== null) { - return extracted; - } - let wrapperRef = javaExceptionWrappers.get(javaException); - if (typeof wrapperRef != "undefined") { - let wrapper = wrapperRef.deref(); - if (typeof wrapper !== "undefined") { - return wrapper; - } - } - let wrapper = new JavaError(javaException); - javaExceptionWrappers.set(javaException, new WeakRef(wrapper)); - return wrapper; - } - } - return e; - } - function jsExceptionAsJava(e) { - if (javaExceptionSymbol in e) { - return e[javaExceptionSymbol]; - } else { - return exports["teavm.js.wrapException"](e); - } - } - function rethrowJsAsJava(e) { - exports["teavm.js.throwException"](jsExceptionAsJava(e)); - } - function extractException(e) { - return exports["teavm.js.extractException"](e); - } - function rethrowJavaAsJs(e) { - throw javaExceptionToJs(e); - } - function getProperty(obj, prop) { - try { - return obj !== null ? obj[prop] : getGlobalName(prop) - } catch (e) { - rethrowJsAsJava(e); - } - } - function defineFunction(fn) { - let params = []; - for (let i = 0; i < fn.length; ++i) { - params.push("p" + i); - } - let paramsAsString = params.length === 0 ? "" : params.join(", "); - return new Function("rethrowJavaAsJs", "fn", - `return function(${paramsAsString}) {\n` + - ` try {\n` + - ` return fn(${paramsAsString});\n` + - ` } catch (e) {\n` + - ` rethrowJavaAsJs(e);\n` + - ` }\n` + - `};` - )(rethrowJavaAsJs, fn); - } - function renameConstructor(name, c) { - return new Function( - "constructor", - `return function ${name}(marker, javaObject) {\n` + - ` return constructor.call(this, marker, javaObject);\n` + - `}\n` - )(c); - } - imports.teavmJso = { - emptyString: () => "", - stringFromCharCode: code => String.fromCharCode(code), - concatStrings: (a, b) => a + b, - stringLength: s => s.length, - charAt: (s, index) => s.charCodeAt(index), - emptyArray: () => [], - appendToArray: (array, e) => array.push(e), - unwrapBoolean: value => value ? 1 : 0, - wrapBoolean: value => !!value, - getProperty: getProperty, - getPropertyPure: getProperty, - setProperty: setProperty, - setPropertyPure: setProperty, - global(name) { - try { - return getGlobalName(name); - } catch (e) { - rethrowJsAsJava(e); - } - }, - createClass(name, parent, constructor) { - name = sanitizeName(name || "JavaObject"); - let action; - if (parent === null) { - action = function (javaObject) { - this[javaObjectSymbol] = javaObject; - this[functionsSymbol] = null; - }; - } else { - action = function (javaObject) { - parent.call(this, javaObject); - }; - fn.prototype = Object.create(parent); - fn.prototype.constructor = parent; - } - let fn = renameConstructor(name, function (marker, javaObject) { - if (marker === wrapperCallMarkerSymbol) { - action.call(this, javaObject); - } else if (constructor === null) { - throw new Error("This class can't be instantiated directly"); - } else { - try { - return constructor.apply(null, arguments); - } catch (e) { - rethrowJavaAsJs(e); - } - } - }); - fn.prototype = Object.create(parent || Object.prototype); - fn.prototype.constructor = fn; - let boundFn = renameConstructor(name, function(javaObject) { - return fn.call(this, wrapperCallMarkerSymbol, javaObject); - }); - boundFn[wrapperCallMarkerSymbol] = fn; - boundFn.prototype = fn.prototype; - return boundFn; - }, - exportClass(cls) { - return cls[wrapperCallMarkerSymbol]; - }, - defineMethod(cls, name, fn) { - let params = []; - for (let i = 1; i < fn.length; ++i) { - params.push("p" + i); - } - let paramsAsString = params.length === 0 ? "" : params.join(", "); - cls.prototype[name] = new Function("rethrowJavaAsJs", "fn", - `return function(${paramsAsString}) {\n` + - ` try {\n` + - ` return fn(${['this', params].join(", ")});\n` + - ` } catch (e) {\n` + - ` rethrowJavaAsJs(e);\n` + - ` }\n` + - `};` - )(rethrowJavaAsJs, fn); - }, - defineStaticMethod(cls, name, fn) { - cls[name] = defineFunction(fn); - }, - defineFunction: defineFunction, - defineProperty(cls, name, getFn, setFn) { - let descriptor = { - get() { - try { - return getFn(this); - } catch (e) { - rethrowJavaAsJs(e); - } - } - }; - if (setFn !== null) { - descriptor.set = function(value) { - try { - setFn(this, value); - } catch (e) { - rethrowJavaAsJs(e); - } - } - } - Object.defineProperty(cls.prototype, name, descriptor); - }, - defineStaticProperty(cls, name, getFn, setFn) { - let descriptor = { - get() { - try { - return getFn(); - } catch (e) { - rethrowJavaAsJs(e); - } - } - }; - if (setFn !== null) { - descriptor.set = function(value) { - try { - setFn(value); - } catch (e) { - rethrowJavaAsJs(e); - } - } - } - Object.defineProperty(cls, name, descriptor); - }, - javaObjectToJS(instance, cls) { - let existing = jsWrappers.get(instance); - if (typeof existing != "undefined") { - let result = existing.deref(); - if (typeof result !== "undefined") { - return result; - } - } - let obj = new cls(instance); - jsWrappers.set(instance, new WeakRef(obj)); - return obj; - }, - unwrapJavaObject(instance) { - return instance[javaObjectSymbol]; - }, - asFunction(instance, propertyName) { - let functions = instance[functionsSymbol]; - if (functions === null) { - functions = Object.create(null); - instance[functionsSymbol] = functions; - } - let result = functions[propertyName]; - if (typeof result !== 'function') { - result = function() { - return instance[propertyName].apply(instance, arguments); - } - result[functionOriginSymbol] = instance; - functions[propertyName] = result; - } - return result; - }, - functionAsObject(fn, property) { - let origin = fn[functionOriginSymbol]; - if (typeof origin !== 'undefined') { - let functions = origin[functionsSymbol]; - if (functions !== void 0 && functions[property] === fn) { - return origin; - } - } - return { - [property]: function(...args) { - try { - return fn(...args); - } catch (e) { - rethrowJavaAsJs(e); - } - } - }; - }, - wrapObject(obj) { - if (obj === null) { - return null; - } - if (typeof obj === "object" || typeof obj === "function" || typeof "obj" === "symbol") { - let result = obj[javaObjectSymbol]; - if (typeof result === "object") { - return result; - } - result = javaWrappers.get(obj); - if (result !== void 0) { - result = result.deref(); - if (result !== void 0) { - return result; - } - } - result = exports["teavm.jso.createWrapper"](obj); - javaWrappers.set(obj, new WeakRef(result)); - return result; - } else { - let result = primitiveWrappers.get(obj); - if (result !== void 0) { - result = result.deref(); - if (result !== void 0) { - return result; - } - } - result = exports["teavm.jso.createWrapper"](obj); - primitiveWrappers.set(obj, new WeakRef(result)); - primitiveFinalization.register(result, obj); - return result; - } - }, - isPrimitive: (value, type) => typeof value === type, - instanceOf: (value, type) => value instanceof type, - instanceOfOrNull: (value, type) => value === null || value instanceof type, - sameRef: (a, b) => a === b, - hashCode: (obj) => { - if (typeof obj === "object" || typeof obj === "function" || typeof obj === "symbol") { - let code = hashCodes.get(obj); - if (typeof code === "number") { - return code; - } - code = nextHashCode(); - hashCodes.set(obj, code); - return code; - } else if (typeof obj === "number") { - return obj | 0; - } else if (typeof obj === "bigint") { - return BigInt.asIntN(obj, 32); - } else if (typeof obj === "boolean") { - return obj ? 1 : 0; - } else { - return 0; - } - }, - apply: (instance, method, args) => { - try { - if (instance === null) { - let fn = getGlobalName(method); - return fn(...args); - } else { - return instance[method](...args); - } - } catch (e) { - rethrowJsAsJava(e); - } - }, - concatArray: (a, b) => a.concat(b), - getJavaException: e => e[javaExceptionSymbol] - }; - for (let name of ["wrapByte", "wrapShort", "wrapChar", "wrapInt", "wrapFloat", "wrapDouble", "unwrapByte", - "unwrapShort", "unwrapChar", "unwrapInt", "unwrapFloat", "unwrapDouble"]) { - imports.teavmJso[name] = identity; - } - function wrapCallFromJavaToJs(call) { - try { - return call(); - } catch (e) { - rethrowJsAsJava(e); - } - } - let argumentList = []; - for (let i = 0; i < 32; ++i) { - let args = argumentList.length === 0 ? "" : argumentList.join(", "); - let argsAndBody = [...argumentList, "body"].join(", "); - imports.teavmJso["createFunction" + i] = new Function("wrapCallFromJavaToJs", ...argumentList, "body", - `return new Function('wrapCallFromJavaToJs', ${argsAndBody}).bind(this, wrapCallFromJavaToJs);` - ).bind(null, wrapCallFromJavaToJs); - imports.teavmJso["callFunction" + i] = new Function("rethrowJsAsJava", "fn", ...argumentList, - `try {\n` + - ` return fn(${args});\n` + - `} catch (e) {\n` + - ` rethrowJsAsJava(e);\n` + - `}` - ).bind(null, rethrowJsAsJava); - imports.teavmJso["callMethod" + i] = new Function("rethrowJsAsJava", "getGlobalName", "instance", - "method", ...argumentList, - `try {\n`+ - ` return instance !== null\n` + - ` ? instance[method](${args})\n` + - ` : getGlobalName(method)(${args});\n` + - `} catch (e) {\n` + - ` rethrowJsAsJava(e);\n` + - `}` - ).bind(null, rethrowJsAsJava, getGlobalName); - imports.teavmJso["construct" + i] = new Function("rethrowJsAsJava", "constructor", ...argumentList, - `try {\n` + - ` return new constructor(${args});\n` + - `} catch (e) {\n` + - ` rethrowJsAsJava(e);\n` + - `}` - ).bind(null, rethrowJsAsJava); - imports.teavmJso["arrayOf" + i] = new Function(...argumentList, "return [" + args + "]"); - - let param = "p" + (i + 1); - argumentList.push(param); - } - } - - function load(path, options) { - if (!options) { - options = {}; - } - - const importObj = {}; - defaults(importObj); - if (typeof options.installImports !== "undefined") { - options.installImports(importObj); - } - - return WebAssembly.instantiateStreaming(fetch(path), importObj) - .then(r => { - exports = r.instance.exports; - let userExports = {}; - let teavm = { - exports: userExports, - instance: r.instance, - module: r.module - }; - for (let key in r.instance.exports) { - let exportObj = r.instance.exports[key]; - if (exportObj instanceof WebAssembly.Global) { - Object.defineProperty(userExports, key, { - get: () => exportObj.value - }); - } - } - return teavm; - }); - } - - return { load, defaults }; -}(); diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index db4ac025d..49b2241ff 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -70,4 +70,5 @@ version.ref = "shadow" shadow = { id = "com.github.johnrengelman.shadow", version.ref = "shadow" } intellij = { id = "org.jetbrains.intellij", version = "1.17.2" } -pluginPublish = { id = "com.gradle.plugin-publish", version = "1.1.0" } \ No newline at end of file +pluginPublish = { id = "com.gradle.plugin-publish", version = "1.1.0" } +nodejs = { id = "com.github.node-gradle.node", version = "7.1.0" } \ No newline at end of file diff --git a/tests/src/test/java/org/teavm/jso/export/ExportTest.java b/tests/src/test/java/org/teavm/jso/export/ExportTest.java index 6b0e654f8..c8196ab81 100644 --- a/tests/src/test/java/org/teavm/jso/export/ExportTest.java +++ b/tests/src/test/java/org/teavm/jso/export/ExportTest.java @@ -203,8 +203,8 @@ public class ExportTest { } var testProviderFile = new File(outputDir, "provider.js"); try (var writer = new OutputStreamWriter(new FileOutputStream(testProviderFile), StandardCharsets.UTF_8)) { - writer.write("await import('/resources/org/teavm/backend/wasm/wasm-gc-runtime.js');\n"); - writer.write("let teavm = await TeaVM.wasmGC.load('/tests/" + name + "/test.wasm');\n"); + writer.write("import { load } from '/resources/org/teavm/backend/wasm/wasm-gc-module-runtime.js';\n"); + writer.write("let teavm = await load('/tests/" + name + "/test.wasm');\n"); writer.write("export default teavm.exports;\n"); } diff --git a/tools/deobfuscator-js/build.gradle.kts b/tools/deobfuscator-js/build.gradle.kts index 3ce8ef535..80013a09a 100644 --- a/tools/deobfuscator-js/build.gradle.kts +++ b/tools/deobfuscator-js/build.gradle.kts @@ -65,7 +65,7 @@ val generateLibJs by tasks.register("generateLibJs") { } val zipWithJs by tasks.register("zipWithJs") { - //dependsOn(generateJs, generateLibJs) + dependsOn(generateJs, generateLibJs) archiveClassifier = "js" from(layout.buildDirectory.dir("teavm"), layout.buildDirectory.dir("teavm-lib")) entryCompression = ZipEntryCompression.DEFLATED diff --git a/tools/gradle/src/main/java/org/teavm/gradle/tasks/CopyWasmGCRuntimeTask.java b/tools/gradle/src/main/java/org/teavm/gradle/tasks/CopyWasmGCRuntimeTask.java index 3516760ca..70adf9946 100644 --- a/tools/gradle/src/main/java/org/teavm/gradle/tasks/CopyWasmGCRuntimeTask.java +++ b/tools/gradle/src/main/java/org/teavm/gradle/tasks/CopyWasmGCRuntimeTask.java @@ -29,7 +29,7 @@ public abstract class CopyWasmGCRuntimeTask extends DefaultTask { @TaskAction public void copyRuntime() throws IOException { - var resourceName = "org/teavm/backend/wasm/wasm-gc-runtime.js"; + var resourceName = "org/teavm/backend/wasm/wasm-gc-runtime.min.js"; var classLoader = CopyWasmGCRuntimeTask.class.getClassLoader(); var output = getOutputFile().get().getAsFile(); try (var input = classLoader.getResourceAsStream(resourceName)) {