Skip to content
This repository has been archived by the owner on Jul 12, 2024. It is now read-only.

Commit

Permalink
Make the linker backend actually produce the specified output shape.
Browse files Browse the repository at this point in the history
In addition to emitting the `.wasm` and `.wat` files in the output
directory, we now also emit emit the internal `__loader.js` file,
and the public module specified by the `ModuleSet`, such as
`main.js`.

We embed the content of the loader as a constant string in the
backend, so that it works both on the JVM and on JS. Eventually, we
will probably generate it more dynamically from
`backend.javascript.Trees` anyway, so this not really a big deal.

This change requires to adapt the loading mechanism to force URL
resolution against the base directory of the `loader.js` file,
rather than the working directly. We do this thanks to
`import.meta.url`, which is also available in browsers.

As is, this change destroys the semantics of top-level exported
`var`s. Mutations performed after the module initializers have
completed will not be reflected in the exports of `main.js`. We
will need to revisit this later on, with a technique similar to the
one used by the JS backend.

With these changes, our linker backend actually abides by the
specification of a `LinkerBackend`, which means we will be able to
directly use it from sbt.
  • Loading branch information
sjrd committed Mar 29, 2024
1 parent f84b2bf commit c42c6bd
Show file tree
Hide file tree
Showing 6 changed files with 76 additions and 20 deletions.
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ lazy val tests = project
"org.scala-js" %%% "scala-js-macrotask-executor" % "1.1.1" % Test
),
scalaJSLinkerConfig ~= {
// Generate CoreTests as an ES module so that it can import the loader.mjs
// Generate CoreTests as an ES module so that it can import the main.mjs files
// Give it an `.mjs` extension so that Node.js actually interprets it as an ES module
_.withModuleKind(ModuleKind.ESModule)
.withOutputPatterns(OutputPatterns.fromJSFile("%s.mjs")),
Expand Down
2 changes: 2 additions & 0 deletions cli/src/main/scala/Main.scala
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ object Main {
val linkerConfig = StandardConfig()
.withESFeatures(_.withESVersion(ESVersion.ES2016)) // to be able to link `**`
.withSemantics(_.optimized) // because that's the only thing we actually support at the moment
.withModuleKind(ModuleKind.ESModule)
.withOptimizer(false)
.withOutputPatterns(OutputPatterns.fromJSFile("%s.mjs"))

val logger = new ScalaConsoleLogger(Level.Info)

Expand Down
9 changes: 4 additions & 5 deletions run.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { load } from "./loader.mjs";
import { test, field } from "./target/sample/main.mjs";

const moduleExports = await load("./target/sample/main.wasm");
console.log(moduleExports.field);
const o = moduleExports.test(7);
console.log(field);
const o = test(7);
console.log(o);
console.log(moduleExports.field);
console.log(field);
7 changes: 1 addition & 6 deletions tests/src/test/scala/tests/CoreTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,10 @@ import org.scalajs.macrotaskexecutor.MacrotaskExecutor.Implicits._
class CoreTests extends munit.FunSuite {
cli.TestSuites.suites.map { suite =>
test(suite.className) {
CoreTests.load(s"./target/${suite.className}/main.wasm").toFuture.map { _ =>
js.`import`[js.Any](s"../../../../target/${suite.className}/main.mjs").toFuture.map { _ =>
()
}
}
}

}

object CoreTests {
@js.native @JSImport("../../../../loader.mjs")
def load(wasmFile: String): js.Promise[js.Dynamic] = js.native
}
50 changes: 46 additions & 4 deletions wasm/src/main/scala/WebAssemblyLinkerBackend.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ final class WebAssemblyLinkerBackend(
linkerConfig: StandardConfig,
val coreSpec: CoreSpec
) extends LinkerBackend {
require(
linkerConfig.moduleKind == ModuleKind.ESModule,
s"The WebAssembly backend only supports ES modules; was ${linkerConfig.moduleKind}."
)
require(
!linkerConfig.optimizer,
"The WebAssembly backend does not support the optimizer yet."
Expand Down Expand Up @@ -89,24 +93,62 @@ final class WebAssemblyLinkerBackend(

val outputImpl = OutputDirectoryImpl.fromOutputDirectory(output)

val watFileName = s"$moduleID.wat"
val wasmFileName = s"$moduleID.wasm"
val jsFileName = OutputPatternsImpl.jsFile(linkerConfig.outputPatterns, moduleID)
val loaderJSFileName = OutputPatternsImpl.jsFile(linkerConfig.outputPatterns, "__loader")

val textOutput = new converters.WasmTextWriter().write(wasmModule)
val textOutputBytes = textOutput.getBytes(StandardCharsets.UTF_8)
val binaryOutput = new converters.WasmBinaryWriter(wasmModule).write()
val loaderOutput = LoaderContent.bytesContent
val jsFileOutput = buildJSFileOutput(onlyModule, loaderJSFileName, wasmFileName)
val jsFileOutputBytes = jsFileOutput.getBytes(StandardCharsets.UTF_8)

val filesToProduce = Set(
watFileName,
wasmFileName,
loaderJSFileName,
jsFileName
)

val filesToProduce = Set(s"$moduleID.wat", s"$moduleID.wasm")
for {
existingFiles <- outputImpl.listFiles()
_ <- Future.sequence(existingFiles.filterNot(filesToProduce).map(outputImpl.delete(_)))
_ <- outputImpl.writeFull(s"$moduleID.wat", ByteBuffer.wrap(textOutputBytes))
_ <- outputImpl.writeFull(s"$moduleID.wasm", ByteBuffer.wrap(binaryOutput))
_ <- outputImpl.writeFull(watFileName, ByteBuffer.wrap(textOutputBytes))
_ <- outputImpl.writeFull(wasmFileName, ByteBuffer.wrap(binaryOutput))
_ <- outputImpl.writeFull(loaderJSFileName, ByteBuffer.wrap(loaderOutput))
_ <- outputImpl.writeFull(jsFileName, ByteBuffer.wrap(jsFileOutputBytes))
} yield {
val reportModule = new ReportImpl.ModuleImpl(
moduleID,
s"$moduleID.wasm",
jsFileName,
None,
linkerConfig.moduleKind
)
new ReportImpl(List(reportModule))
}
}

private def buildJSFileOutput(
module: ModuleSet.Module,
loaderJSFileName: String,
wasmFileName: String
): String = {
/* TODO This is not correct for exported *vars*, since they won't receive
* updates from mutations after loading.
*/
val reExportStats = for {
exportName <- module.topLevelExports.map(_.exportName)
} yield {
s"export let $exportName = __exports.$exportName;"
}

s"""
|import { load as __load } from './${loaderJSFileName}';
|const __exports = await __load('./${wasmFileName}');
|
|${reExportStats.mkString("\n")}
""".stripMargin.trim() + "\n"
}
}
26 changes: 22 additions & 4 deletions loader.mjs → ...rc/main/scala/ir2wasm/LoaderContent.scala
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
package wasm.ir2wasm

import java.nio.charset.StandardCharsets

/** Contents of the `__loader.js` file that we emit in every output. */
object LoaderContent {
val bytesContent: Array[Byte] =
stringContent.getBytes(StandardCharsets.UTF_8)

private def stringContent: String = {
"""
// Specified by java.lang.String.hashCode()
function stringHashCode(s) {
var res = 0;
Expand Down Expand Up @@ -167,17 +178,21 @@ const scalaJSHelpers = {
jsExponent: (a, b) => a ** b,
}
export async function load(wasmFileName) {
export async function load(wasmFileURL) {
const importsObj = {
"__scalaJSHelpers": scalaJSHelpers,
};
const resolvedURL = new URL(wasmFileURL, import.meta.url);
var wasmModulePromise;
if (typeof process !== "undefined") {
if (resolvedURL.protocol === 'file:') {
const wasmPath = import("node:url").then((url) => url.fileURLToPath(resolvedURL))
wasmModulePromise = import("node:fs").then((fs) => {
return WebAssembly.instantiate(fs.readFileSync(wasmFileName), importsObj);
return wasmPath.then((path) => {
return WebAssembly.instantiate(fs.readFileSync(path), importsObj);
});
});
} else {
wasmModulePromise = WebAssembly.instantiateStreaming(fetch(wasmFileName), importsObj);
wasmModulePromise = WebAssembly.instantiateStreaming(fetch(resolvedURL), importsObj);
}
const wasmModule = await wasmModulePromise;
const exports = wasmModule.instance.exports;
Expand All @@ -197,4 +212,7 @@ export async function load(wasmFileName) {
}
Object.freeze(userExports);
return userExports;
}
"""
}
}

0 comments on commit c42c6bd

Please sign in to comment.