[Wasm] Add uninstantiated MJS wrapper
It allows * Custom imports * Ability to skip initializer
This commit is contained in:
@@ -368,22 +368,22 @@ class K2JsIrCompiler : CLICompiler<K2JSCompilerArguments>() {
|
|||||||
eliminateDeadDeclarations(allModules, backendContext)
|
eliminateDeadDeclarations(allModules, backendContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
val sourceMapFileName = if (configuration.getBoolean(JSConfigurationKeys.SOURCE_MAP)) "$outputName.map" else null
|
val generateSourceMaps = configuration.getBoolean(JSConfigurationKeys.SOURCE_MAP)
|
||||||
|
|
||||||
val res = compileWasm(
|
val res = compileWasm(
|
||||||
allModules = allModules,
|
allModules = allModules,
|
||||||
backendContext = backendContext,
|
backendContext = backendContext,
|
||||||
|
baseFileName = outputName,
|
||||||
emitNameSection = arguments.wasmDebug,
|
emitNameSection = arguments.wasmDebug,
|
||||||
allowIncompleteImplementations = arguments.irDce,
|
allowIncompleteImplementations = arguments.irDce,
|
||||||
generateWat = true,
|
generateWat = true,
|
||||||
sourceMapFileName = sourceMapFileName
|
generateSourceMaps = generateSourceMaps
|
||||||
)
|
)
|
||||||
|
|
||||||
writeCompilationResult(
|
writeCompilationResult(
|
||||||
result = res,
|
result = res,
|
||||||
dir = outputDir,
|
dir = outputDir,
|
||||||
fileNameBase = outputName,
|
fileNameBase = outputName,
|
||||||
sourceMapFileName = sourceMapFileName
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return OK
|
return OK
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ import java.io.File
|
|||||||
|
|
||||||
class WasmCompilerResult(
|
class WasmCompilerResult(
|
||||||
val wat: String?,
|
val wat: String?,
|
||||||
val js: String,
|
val jsUninstantiatedWrapper: String,
|
||||||
|
val jsWrapper: String,
|
||||||
val wasm: ByteArray,
|
val wasm: ByteArray,
|
||||||
val sourceMap: String?
|
val sourceMap: String?
|
||||||
)
|
)
|
||||||
@@ -86,10 +87,11 @@ fun compileToLoweredIr(
|
|||||||
fun compileWasm(
|
fun compileWasm(
|
||||||
allModules: List<IrModuleFragment>,
|
allModules: List<IrModuleFragment>,
|
||||||
backendContext: WasmBackendContext,
|
backendContext: WasmBackendContext,
|
||||||
|
baseFileName: String,
|
||||||
emitNameSection: Boolean = false,
|
emitNameSection: Boolean = false,
|
||||||
allowIncompleteImplementations: Boolean = false,
|
allowIncompleteImplementations: Boolean = false,
|
||||||
generateWat: Boolean = false,
|
generateWat: Boolean = false,
|
||||||
sourceMapFileName: String? = null,
|
generateSourceMaps: Boolean = false,
|
||||||
): WasmCompilerResult {
|
): WasmCompilerResult {
|
||||||
val compiledWasmModule = WasmCompiledModuleFragment(backendContext.irBuiltIns)
|
val compiledWasmModule = WasmCompiledModuleFragment(backendContext.irBuiltIns)
|
||||||
val codeGenerator = WasmModuleFragmentGenerator(backendContext, compiledWasmModule, allowIncompleteImplementations = allowIncompleteImplementations)
|
val codeGenerator = WasmModuleFragmentGenerator(backendContext, compiledWasmModule, allowIncompleteImplementations = allowIncompleteImplementations)
|
||||||
@@ -105,12 +107,14 @@ fun compileWasm(
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
val js = compiledWasmModule.generateJs()
|
val jsUninstantiatedWrapper = compiledWasmModule.generateAsyncJsWrapper("./$baseFileName.wasm")
|
||||||
|
val jsWrapper = generateEsmExportsWrapper("./$baseFileName.uninstantiated.mjs")
|
||||||
|
|
||||||
val os = ByteArrayOutputStream()
|
val os = ByteArrayOutputStream()
|
||||||
|
|
||||||
|
val sourceMapFileName = "$baseFileName.map".takeIf { generateSourceMaps }
|
||||||
val sourceLocationMappings =
|
val sourceLocationMappings =
|
||||||
if (sourceMapFileName != null) mutableListOf<SourceLocationMapping>() else null
|
if (generateSourceMaps) mutableListOf<SourceLocationMapping>() else null
|
||||||
|
|
||||||
val wasmIrToBinary =
|
val wasmIrToBinary =
|
||||||
WasmIrToBinary(
|
WasmIrToBinary(
|
||||||
@@ -128,7 +132,8 @@ fun compileWasm(
|
|||||||
|
|
||||||
return WasmCompilerResult(
|
return WasmCompilerResult(
|
||||||
wat = wat,
|
wat = wat,
|
||||||
js = js,
|
jsUninstantiatedWrapper = jsUninstantiatedWrapper,
|
||||||
|
jsWrapper = jsWrapper,
|
||||||
wasm = byteArray,
|
wasm = byteArray,
|
||||||
sourceMap = generateSourceMap(backendContext.configuration, sourceLocationMappings)
|
sourceMap = generateSourceMap(backendContext.configuration, sourceLocationMappings)
|
||||||
)
|
)
|
||||||
@@ -167,46 +172,43 @@ private fun generateSourceMap(
|
|||||||
return sourceMapBuilder.build()
|
return sourceMapBuilder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun WasmCompiledModuleFragment.generateJs(): String {
|
fun WasmCompiledModuleFragment.generateAsyncJsWrapper(wasmFilePath: String): String {
|
||||||
//language=js
|
val jsCodeBody = jsFuns.joinToString(",\n") {
|
||||||
val runtime = """
|
"${it.importName.toJsStringLiteral()} : ${it.jsCode}"
|
||||||
const externrefBoxes = new WeakMap();
|
}
|
||||||
// ref must be non-null
|
|
||||||
function tryGetOrSetExternrefBox(ref, ifNotCached) {
|
val jsCodeBodyIndented = jsCodeBody.prependIndent(" ")
|
||||||
if (typeof ref !== 'object') return ifNotCached;
|
|
||||||
const cachedBox = externrefBoxes.get(ref);
|
|
||||||
if (cachedBox !== void 0) return cachedBox;
|
|
||||||
externrefBoxes.set(ref, ifNotCached);
|
|
||||||
return ifNotCached;
|
|
||||||
} """.trimIndent()
|
|
||||||
|
|
||||||
val imports = jsModuleImports
|
val imports = jsModuleImports
|
||||||
.toList()
|
.toList()
|
||||||
.sorted()
|
.sorted()
|
||||||
.joinToString("\n") {
|
.joinToString("") {
|
||||||
val moduleSpecifier = it.toJsStringLiteral()
|
val moduleSpecifier = it.toJsStringLiteral()
|
||||||
" $moduleSpecifier: await import($moduleSpecifier),"
|
" $moduleSpecifier: imports[$moduleSpecifier] ?? await import($moduleSpecifier),\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
val jsCodeBody = jsFuns.joinToString(",\n") {
|
//language=js
|
||||||
"${it.importName.toJsStringLiteral()} : ${it.jsCode}"
|
return """
|
||||||
}
|
const externrefBoxes = new WeakMap();
|
||||||
val jsCodeBodyIndented = jsCodeBody.prependIndent(" ")
|
// ref must be non-null
|
||||||
val importObject = """
|
function tryGetOrSetExternrefBox(ref, ifNotCached) {
|
||||||
const _import_object = {
|
if (typeof ref !== 'object') return ifNotCached;
|
||||||
${imports}
|
const cachedBox = externrefBoxes.get(ref);
|
||||||
js_code: {
|
if (cachedBox !== void 0) return cachedBox;
|
||||||
${jsCodeBodyIndented}
|
externrefBoxes.set(ref, ifNotCached);
|
||||||
}
|
return ifNotCached;
|
||||||
};
|
|
||||||
"""
|
|
||||||
|
|
||||||
return runtime + importObject
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun generateJsWasmLoader(wasmFilePath: String, externalJs: String): String =
|
const js_code = {
|
||||||
externalJs + """
|
$jsCodeBodyIndented
|
||||||
|
}
|
||||||
|
|
||||||
|
// Placed here to give access to it from externals (js_code)
|
||||||
|
let wasmInstance;
|
||||||
|
let require;
|
||||||
|
let wasmExports;
|
||||||
|
|
||||||
|
export async function instantiate(imports={}, runInitializer=true) {
|
||||||
const isNodeJs = (typeof process !== 'undefined') && (process.release.name === 'node');
|
const isNodeJs = (typeof process !== 'undefined') && (process.release.name === 'node');
|
||||||
const isD8 = !isNodeJs && (typeof d8 !== 'undefined');
|
const isD8 = !isNodeJs && (typeof d8 !== 'undefined');
|
||||||
const isBrowser = !isNodeJs && !isD8 && (typeof window !== 'undefined');
|
const isBrowser = !isNodeJs && !isD8 && (typeof window !== 'undefined');
|
||||||
@@ -215,8 +217,12 @@ fun generateJsWasmLoader(wasmFilePath: String, externalJs: String): String =
|
|||||||
throw "Supported JS engine not detected";
|
throw "Supported JS engine not detected";
|
||||||
}
|
}
|
||||||
|
|
||||||
let wasmInstance;
|
const wasmFilePath = ${wasmFilePath.toJsStringLiteral()};
|
||||||
let require; // Placed here to give access to it from externals (js_code)
|
const importObject = {
|
||||||
|
js_code,
|
||||||
|
$imports
|
||||||
|
};
|
||||||
|
|
||||||
if (isNodeJs) {
|
if (isNodeJs) {
|
||||||
const module = await import(/* webpackIgnore: true */'node:module');
|
const module = await import(/* webpackIgnore: true */'node:module');
|
||||||
require = module.default.createRequire(import.meta.url);
|
require = module.default.createRequire(import.meta.url);
|
||||||
@@ -225,31 +231,40 @@ fun generateJsWasmLoader(wasmFilePath: String, externalJs: String): String =
|
|||||||
const url = require('url');
|
const url = require('url');
|
||||||
const filepath = url.fileURLToPath(import.meta.url);
|
const filepath = url.fileURLToPath(import.meta.url);
|
||||||
const dirpath = path.dirname(filepath);
|
const dirpath = path.dirname(filepath);
|
||||||
const wasmBuffer = fs.readFileSync(path.resolve(dirpath, '$wasmFilePath'));
|
const wasmBuffer = fs.readFileSync(path.resolve(dirpath, wasmFilePath));
|
||||||
const wasmModule = new WebAssembly.Module(wasmBuffer);
|
const wasmModule = new WebAssembly.Module(wasmBuffer);
|
||||||
wasmInstance = new WebAssembly.Instance(wasmModule, _import_object);
|
wasmInstance = new WebAssembly.Instance(wasmModule, importObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isD8) {
|
if (isD8) {
|
||||||
const wasmBuffer = read('$wasmFilePath', 'binary');
|
const wasmBuffer = read(wasmFilePath, 'binary');
|
||||||
const wasmModule = new WebAssembly.Module(wasmBuffer);
|
const wasmModule = new WebAssembly.Module(wasmBuffer);
|
||||||
wasmInstance = new WebAssembly.Instance(wasmModule, _import_object);
|
wasmInstance = new WebAssembly.Instance(wasmModule, importObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isBrowser) {
|
if (isBrowser) {
|
||||||
wasmInstance = (await WebAssembly.instantiateStreaming(fetch('$wasmFilePath'), _import_object)).instance;
|
wasmInstance = (await WebAssembly.instantiateStreaming(fetch(wasmFilePath), importObject)).instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
const wasmExports = wasmInstance.exports;
|
wasmExports = wasmInstance.exports;
|
||||||
wasmExports.__init();
|
if (runInitializer) {
|
||||||
export default wasmExports;
|
wasmExports.__init();
|
||||||
""".trimIndent()
|
}
|
||||||
|
|
||||||
|
return { instance: wasmInstance, exports: wasmExports };
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generateEsmExportsWrapper(asyncWrapperFileName: String): String = /*language=js */ """
|
||||||
|
import { instantiate } from ${asyncWrapperFileName.toJsStringLiteral()};
|
||||||
|
export default (await instantiate()).exports;
|
||||||
|
"""
|
||||||
|
|
||||||
fun writeCompilationResult(
|
fun writeCompilationResult(
|
||||||
result: WasmCompilerResult,
|
result: WasmCompilerResult,
|
||||||
dir: File,
|
dir: File,
|
||||||
fileNameBase: String,
|
fileNameBase: String
|
||||||
sourceMapFileName: String?
|
|
||||||
) {
|
) {
|
||||||
dir.mkdirs()
|
dir.mkdirs()
|
||||||
if (result.wat != null) {
|
if (result.wat != null) {
|
||||||
@@ -257,10 +272,10 @@ fun writeCompilationResult(
|
|||||||
}
|
}
|
||||||
File(dir, "$fileNameBase.wasm").writeBytes(result.wasm)
|
File(dir, "$fileNameBase.wasm").writeBytes(result.wasm)
|
||||||
|
|
||||||
val jsWithLoader = generateJsWasmLoader("./$fileNameBase.wasm", result.js)
|
File(dir, "$fileNameBase.uninstantiated.mjs").writeText(result.jsUninstantiatedWrapper)
|
||||||
File(dir, "$fileNameBase.mjs").writeText(jsWithLoader)
|
File(dir, "$fileNameBase.mjs").writeText(result.jsWrapper)
|
||||||
|
|
||||||
if (sourceMapFileName != null) {
|
if (result.sourceMap != null) {
|
||||||
File(dir, sourceMapFileName).writeText(result.sourceMap!!)
|
File(dir, "$fileNameBase.map").writeText(result.sourceMap)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
// TARGET_BACKEND: WASM
|
||||||
|
|
||||||
|
// FILE: wasmImport.kt
|
||||||
|
import kotlin.wasm.WasmImport
|
||||||
|
|
||||||
|
@WasmImport("foo")
|
||||||
|
external fun inc1(x: Int): Int
|
||||||
|
|
||||||
|
@WasmImport("~!@#\$%^&*()_+`-={}|[]\\\\:\\\";'<>?,./", "inc2")
|
||||||
|
external fun inc2(x: Int): Int
|
||||||
|
|
||||||
|
@WasmImport("./bar.mjs", "inc3")
|
||||||
|
external fun inc3(x: Int): Int
|
||||||
|
|
||||||
|
@JsExport
|
||||||
|
fun myBox(): String {
|
||||||
|
if (inc1(5) != 6) return "KFail1"
|
||||||
|
if (inc2(5) != 6) return "KFail2"
|
||||||
|
if (inc3(5) != 6) return "KFail3"
|
||||||
|
return "OK"
|
||||||
|
}
|
||||||
|
|
||||||
|
var initialized: Int = 0
|
||||||
|
|
||||||
|
@JsExport
|
||||||
|
fun getInitialized(): Int = initialized
|
||||||
|
|
||||||
|
fun main() {
|
||||||
|
initialized = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
// FILE: entry.mjs
|
||||||
|
import { instantiate } from "./index.uninstantiated.mjs";
|
||||||
|
|
||||||
|
let inc = x => x + 1;
|
||||||
|
|
||||||
|
let imports = {
|
||||||
|
"foo": { inc1 : inc },
|
||||||
|
"~!@#\$%^&*()_+\`-={}|[]\\\\:\\\";'<>?,./" : { inc2 : inc },
|
||||||
|
"./bar.mjs" : { inc3 : inc },
|
||||||
|
}
|
||||||
|
|
||||||
|
let { exports } = await instantiate(imports);
|
||||||
|
|
||||||
|
if (exports.getInitialized() !== 100) {
|
||||||
|
throw "Fail1"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exports.myBox() != "OK") {
|
||||||
|
throw "Fail2"
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
// TARGET_BACKEND: WASM
|
||||||
|
|
||||||
|
// FILE: wasmImport.kt
|
||||||
|
import kotlin.wasm.WasmImport
|
||||||
|
|
||||||
|
@WasmImport("foo")
|
||||||
|
external fun inc(x: Int): Int
|
||||||
|
|
||||||
|
@JsExport
|
||||||
|
fun myBox(): String {
|
||||||
|
if (inc(5) != 6) return "KFail1"
|
||||||
|
return "OK"
|
||||||
|
}
|
||||||
|
|
||||||
|
var initialized: Int = 0
|
||||||
|
|
||||||
|
@JsExport
|
||||||
|
fun getInitialized(): Int = initialized
|
||||||
|
|
||||||
|
fun main() {
|
||||||
|
initialized = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
// FILE: entry.mjs
|
||||||
|
import { instantiate } from "./index.uninstantiated.mjs";
|
||||||
|
|
||||||
|
let inc = x => x + 1;
|
||||||
|
|
||||||
|
let imports = {
|
||||||
|
"foo": { inc },
|
||||||
|
}
|
||||||
|
|
||||||
|
let { exports } = await instantiate(imports, /*runInitializer=*/false);
|
||||||
|
if (exports.getInitialized() !== 0) {
|
||||||
|
throw "Fail1"
|
||||||
|
}
|
||||||
|
exports.__init();
|
||||||
|
if (exports.getInitialized() !== 100) {
|
||||||
|
throw "Fail2"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exports.myBox() != "OK") {
|
||||||
|
throw "Fail3"
|
||||||
|
}
|
||||||
@@ -69,6 +69,8 @@ abstract class BasicWasmBoxTest(
|
|||||||
val jsFilesAfter = mutableListOf<String>()
|
val jsFilesAfter = mutableListOf<String>()
|
||||||
val mjsFiles = mutableListOf<String>()
|
val mjsFiles = mutableListOf<String>()
|
||||||
|
|
||||||
|
var entryMjs: String? = "test.mjs"
|
||||||
|
|
||||||
inputFiles.forEach {
|
inputFiles.forEach {
|
||||||
val name = it.fileName
|
val name = it.fileName
|
||||||
when {
|
when {
|
||||||
@@ -81,8 +83,13 @@ abstract class BasicWasmBoxTest(
|
|||||||
name.endsWith(".js") ->
|
name.endsWith(".js") ->
|
||||||
jsFilesBefore += name
|
jsFilesBefore += name
|
||||||
|
|
||||||
name.endsWith(".mjs") ->
|
name.endsWith(".mjs") -> {
|
||||||
mjsFiles += name
|
mjsFiles += name
|
||||||
|
val fileName = File(name).name
|
||||||
|
if (fileName == "entry.mjs") {
|
||||||
|
entryMjs = fileName
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,10 +143,12 @@ abstract class BasicWasmBoxTest(
|
|||||||
)
|
)
|
||||||
|
|
||||||
val generateWat = debugMode >= DebugMode.DEBUG
|
val generateWat = debugMode >= DebugMode.DEBUG
|
||||||
|
val baseFileName = "index"
|
||||||
|
|
||||||
val compilerResult = compileWasm(
|
val compilerResult = compileWasm(
|
||||||
allModules = allModules,
|
allModules = allModules,
|
||||||
backendContext = backendContext,
|
backendContext = backendContext,
|
||||||
|
baseFileName = baseFileName,
|
||||||
emitNameSection = true,
|
emitNameSection = true,
|
||||||
allowIncompleteImplementations = false,
|
allowIncompleteImplementations = false,
|
||||||
generateWat = generateWat,
|
generateWat = generateWat,
|
||||||
@@ -150,6 +159,7 @@ abstract class BasicWasmBoxTest(
|
|||||||
val compilerResultWithDCE = compileWasm(
|
val compilerResultWithDCE = compileWasm(
|
||||||
allModules = allModules,
|
allModules = allModules,
|
||||||
backendContext = backendContext,
|
backendContext = backendContext,
|
||||||
|
baseFileName = baseFileName,
|
||||||
emitNameSection = true,
|
emitNameSection = true,
|
||||||
allowIncompleteImplementations = true,
|
allowIncompleteImplementations = true,
|
||||||
generateWat = generateWat,
|
generateWat = generateWat,
|
||||||
@@ -189,6 +199,7 @@ abstract class BasicWasmBoxTest(
|
|||||||
val path = dir.absolutePath
|
val path = dir.absolutePath
|
||||||
println(" ------ $name Wat file://$path/index.wat")
|
println(" ------ $name Wat file://$path/index.wat")
|
||||||
println(" ------ $name Wasm file://$path/index.wasm")
|
println(" ------ $name Wasm file://$path/index.wasm")
|
||||||
|
println(" ------ $name JS file://$path/index.uninstantiated.mjs")
|
||||||
println(" ------ $name JS file://$path/index.mjs")
|
println(" ------ $name JS file://$path/index.mjs")
|
||||||
println(" ------ $name Test file://$path/test.mjs")
|
println(" ------ $name Test file://$path/test.mjs")
|
||||||
val projectName = "kotlin"
|
val projectName = "kotlin"
|
||||||
@@ -221,7 +232,7 @@ abstract class BasicWasmBoxTest(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
writeCompilationResult(res, dir, "index", sourceMapFileName = null)
|
writeCompilationResult(res, dir, baseFileName)
|
||||||
File(dir, "test.mjs").writeText(testJs)
|
File(dir, "test.mjs").writeText(testJs)
|
||||||
|
|
||||||
for (mjsPath: String in mjsFiles) {
|
for (mjsPath: String in mjsFiles) {
|
||||||
@@ -234,7 +245,7 @@ abstract class BasicWasmBoxTest(
|
|||||||
"--experimental-wasm-gc",
|
"--experimental-wasm-gc",
|
||||||
*jsFilesBefore.map { File(it).absolutePath }.toTypedArray(),
|
*jsFilesBefore.map { File(it).absolutePath }.toTypedArray(),
|
||||||
"--module",
|
"--module",
|
||||||
"./test.mjs",
|
"./${entryMjs}",
|
||||||
*jsFilesAfter.map { File(it).absolutePath }.toTypedArray(),
|
*jsFilesAfter.map { File(it).absolutePath }.toTypedArray(),
|
||||||
workingDirectory = dir
|
workingDirectory = dir
|
||||||
)
|
)
|
||||||
|
|||||||
+10
@@ -55,6 +55,16 @@ public class IrCodegenWasmJsInteropWasmTestGenerated extends AbstractIrCodegenWa
|
|||||||
runTest("compiler/testData/codegen/boxWasmJsInterop/functionTypes.kt");
|
runTest("compiler/testData/codegen/boxWasmJsInterop/functionTypes.kt");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@TestMetadata("imperativeWrapperInitialised.kt")
|
||||||
|
public void testImperativeWrapperInitialised() throws Exception {
|
||||||
|
runTest("compiler/testData/codegen/boxWasmJsInterop/imperativeWrapperInitialised.kt");
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestMetadata("imperativeWrapperUninitialised.kt")
|
||||||
|
public void testImperativeWrapperUninitialised() throws Exception {
|
||||||
|
runTest("compiler/testData/codegen/boxWasmJsInterop/imperativeWrapperUninitialised.kt");
|
||||||
|
}
|
||||||
|
|
||||||
@TestMetadata("jsExport.kt")
|
@TestMetadata("jsExport.kt")
|
||||||
public void testJsExport() throws Exception {
|
public void testJsExport() throws Exception {
|
||||||
runTest("compiler/testData/codegen/boxWasmJsInterop/jsExport.kt");
|
runTest("compiler/testData/codegen/boxWasmJsInterop/jsExport.kt");
|
||||||
|
|||||||
Reference in New Issue
Block a user