From 4d9423d3a8d531320d29c93c551f268f869028e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 29 Mar 2024 19:50:05 +0100 Subject: [PATCH] Actually use our linker as the linker for `sample/fastLinkJS`. Now, `sample/fastLinkJS` uses our WebAssembly linker, and `sample/run` actually executes the `main` method of the sample project, linked to WebAssembly. --- .github/workflows/ci.yml | 2 - README.md | 63 ++++++++++++++++++++---------- build.sbt | 18 +++------ project/WasmLinkerPlugin.scala | 51 ++++++++++++++++++++++++ run.mjs | 2 +- sample/src/main/scala/Sample.scala | 4 ++ 6 files changed, 103 insertions(+), 37 deletions(-) create mode 100644 project/WasmLinkerPlugin.scala diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 12768a59..99cce498 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,5 @@ jobs: run: sbt tests/test - name: Run the Sample run: sbt sample/run - - name: Compile wasmJVM - run: sbt wasmJVM/compile - name: Format run: sbt scalafmtCheckAll diff --git a/README.md b/README.md index e1d317de..8ce3e00b 100644 --- a/README.md +++ b/README.md @@ -2,40 +2,61 @@ [![CI](https://github.com/tanishiking/scala-wasm/actions/workflows/ci.yml/badge.svg)](https://github.com/tanishiking/scala-wasm/actions/workflows/ci.yml) -### Usage +### Prerequisites -- first, `npm install` -- then `sample/run` to compile `sample/src/main/scala/Sample.scala`. - - prints the WebAssembly Text Format (WAT) (Stack IR form) to the console, for debugging - - writes the binary format (WASM) to `target/output.wasm` +This project requires Node.js >= 22, which is available as nightly builds as of this writing. +This is necessary to get enough support of WasmGC. -Run the binary using through `run.js` using a JavaScript engine that supports WasmGC, such as Deno +If you are using NVM, you can instal Node.js 22 as follows: ```sh -$ deno run --allow-read run.mjs +# Enable resolution of nightly builds +$ NVM_NODEJS_ORG_MIRROR=https://nodejs.org/download/nightly nvm install v22 +# Switch to Node.js 22 +$ nvm use 22 ``` -### Debugging tools +Otherwise, you can [manually download nightly builds of Node.js](https://nodejs.org/download/nightly/). -- The WasmGC reference interpreter can be used to validate and convert between the binary and text form: - - https://github.com/WebAssembly/gc/tree/main/interpreter - - Use docker image for it https://github.com/tanishiking/wasmgc-docker +### Setup -### Testing +Before doing anything else, run `npm install`. -Requires NodeJS >= 22 (for enough support of WasmGC). +### Run the sample -```sh -# if you are using NVM, you can install Node 22 with -$ NVM_NODEJS_ORG_MIRROR=https://nodejs.org/download/nightly nvm install v22 -# switch to use Node 22 -$ nvm use 22 -``` +In `sample/src/main/scala/Sample.scala` you can find a sandbox to play around. + +You can build and run it like any other Scala.js project from sbt: + +- `sample/fastLinkJS` compiles and links the project with the WebAssembly backend. +- `sample/run` runs the sample linked to WebAssembly with `node`. + +You may want to look at the output in `sample/target/scala-2.12/sample-fastopt/` to convince yourself that it was compiled to WebAssembly. + +In that directory, you will also find a `main.wat` file, which is not used for execution. +It contains the WebAsembly Text Format representation of `main.wasm`, for debugging purposes. + +:warning: If you modify the linker code, you need to `reload` and `sample/clean` for your changes to take effect on the sample. + +You can also use the `run.mjs` script to play with `@JSExportTopLevel` exports. + +- Run from the command line with `node run.mjs`. +- Run from the browser by starting an HTTP server (e.g., `python -m http.server`) and navigate to `testrun.html`. + +### Test suite + +Run the test suite with `tests/test`. - `tests/test` will - Run `testSuite/run` to compile the Scala code under `test-suite` to WebAssembly - Run the WebAssembly binary using NodeJS -- Each Scala program in `test-suite` should have a function that has no arguments and return a Boolean value. The test passes if the function returns `true`. -- When you add a test, +- Each Scala program in `test-suite` should have a `def main(): Unit` function. The test passes if the function successfully executes without throwing. +- When you add a test, - Add a file under `test-suite` - Add a test case to `cli/src/main/scala/TestSuites.scala` (`methodName` should be a exported function name). + +### Debugging tools + +- The WasmGC reference interpreter can be used to validate and convert between the binary and text form: + - https://github.com/WebAssembly/gc/tree/main/interpreter + - Use docker image for it https://github.com/tanishiking/wasmgc-docker diff --git a/build.sbt b/build.sbt index 990b9196..58b4e349 100644 --- a/build.sbt +++ b/build.sbt @@ -3,6 +3,10 @@ import org.scalajs.linker.interface.OutputPatterns val scalaV = "2.12.19" +// Include wasm.jvm on the classpath used to dynamically load Scala.js linkers +Global / scalaJSLinkerImpl / fullClasspath := + (wasm.jvm / Compile / fullClasspath).value + inThisBuild(Def.settings( scalacOptions ++= Seq( "-encoding", @@ -49,22 +53,10 @@ lazy val wasm = crossProject(JVMPlatform, JSPlatform) lazy val sample = project .in(file("sample")) - .enablePlugins(ScalaJSPlugin) + .enablePlugins(WasmLinkerPlugin) .settings( scalaVersion := scalaV, scalaJSUseMainModuleInitializer := true, - Compile / jsEnv := { - import org.scalajs.jsenv.nodejs.NodeJSEnv - val cp = Attributed - .data((Compile / fullClasspath).value) - .mkString(";") - val env = Map( - "SCALAJS_CLASSPATH" -> cp, - "SCALAJS_MODE" -> "sample", - ) - new NodeJSEnv(NodeJSEnv.Config().withEnv(env).withArgs(List("--enable-source-maps"))) - }, - Compile / jsEnvInput := (`cli` / Compile / jsEnvInput).value ) lazy val testSuite = project diff --git a/project/WasmLinkerPlugin.scala b/project/WasmLinkerPlugin.scala new file mode 100644 index 00000000..a93a3f4f --- /dev/null +++ b/project/WasmLinkerPlugin.scala @@ -0,0 +1,51 @@ +package build + +import sbt._ +import sbt.Keys._ + +import org.scalajs.linker._ +import org.scalajs.linker.interface.{ModuleKind, _} + +import org.scalajs.sbtplugin._ +import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._ + +object WasmLinkerPlugin extends AutoPlugin { + override lazy val requires = ScalaJSPlugin + + /** A `LinkerImpl` that reflectively loads our `WebAssemblyLinkerImpl`. */ + private class WasmLinkerImpl(base: LinkerImpl.Reflect) + extends LinkerImpl.Forwarding(base) { + + private val loader = base.loader + + private val clearableLinkerMethod = { + Class.forName("wasm.WebAssemblyLinkerImpl", true, loader) + .getMethod("clearableLinker", classOf[StandardConfig]) + } + + override def clearableLinker(config: StandardConfig): ClearableLinker = + clearableLinkerMethod.invoke(null, config).asInstanceOf[ClearableLinker] + } + + override def projectSettings: Seq[Setting[_]] = Def.settings( + // Use a separate cache box for the LinkerImpl in this project (don't use the Global one) + scalaJSLinkerImplBox := new CacheBox, + + // Use our custom WasmLinkerImpl as the linker implementation used by fast/fullLinkJS + scalaJSLinkerImpl := { + val cp = (scalaJSLinkerImpl / fullClasspath).value + scalaJSLinkerImplBox.value.ensure { + new WasmLinkerImpl(LinkerImpl.reflect(Attributed.data(cp))) + } + }, + + // Automatically install all the configs required by the Wasm backend + scalaJSLinkerConfig ~= { prev => + prev + .withModuleKind(ModuleKind.ESModule) + .withSemantics(_.optimized) + .withOutputPatterns(OutputPatterns.fromJSFile("%s.mjs")) + .withOptimizer(false) + }, + ) +} diff --git a/run.mjs b/run.mjs index b3b16ef7..9ca38574 100644 --- a/run.mjs +++ b/run.mjs @@ -1,4 +1,4 @@ -import { test, field } from "./target/sample/main.mjs"; +import { test, field } from "./sample/target/scala-2.12/sample-fastopt/main.mjs"; console.log(field); const o = test(7); diff --git a/sample/src/main/scala/Sample.scala b/sample/src/main/scala/Sample.scala index 13380cc2..b890bf4e 100644 --- a/sample/src/main/scala/Sample.scala +++ b/sample/src/main/scala/Sample.scala @@ -15,6 +15,10 @@ object Main { true } + def main(args: Array[String]): Unit = { + println("hello world") + } + private def println(x: Any): Unit = js.Dynamic.global.console.log("" + x) }