ghc-wasm-bindgen 0.2.0.0 → 0.3.0.0
raw patch · 9 files changed
+70/−50 lines, 9 files
Files
- README.md +21/−9
- app/Main.hs +3/−2
- ghc-wasm-bindgen.cabal +1/−3
- runtime-js/ghc_wasm_bindgen_internal.d.ts +4/−3
- runtime-js/ghc_wasm_bindgen_internal.js +14/−13
- runtime-js/ghc_wasm_bindgen_runtime.d.ts +0/−5
- runtime-js/ghc_wasm_bindgen_runtime.js +0/−3
- src/GhcWasmBindgen.hs +2/−3
- src/GhcWasmBindgen/EmitJavaScript.hs +25/−9
README.md view
@@ -10,9 +10,7 @@ ```ts import { area, Circle, largest, Rect, scale } from "shape";-import { init } from "shape/runtime"; -await init(wasmUrl); area(Rect({ width: 3, height: 4 })); // 12 scale(2, Circle(1)); // { $tag: "Circle", contents: 2 } largest([]); // null@@ -59,7 +57,7 @@ - An argument that does not fit its type (an out-of-range integer, a `Char` that is not one code point, an unknown constructor, …) throws a `TypeError` before reaching Haskell.-- An exception in Haskell throws a `HaskellError`.+- An exception in Haskell throws a `HaskellError` (exported by the bindings). ## Usage @@ -85,16 +83,11 @@ "version": "0.1.0", "type": "module", "exports": {- ".": { "types": "./src/shape.d.ts", "default": "./src/shape.js" },- "./runtime": { "types": "./src/ghc_wasm_bindgen_runtime.d.ts", "default": "./src/ghc_wasm_bindgen_runtime.js" },- "./shape.wasm": "./src/shape.wasm"+ ".": { "types": "./src/shape.d.ts", "default": "./src/shape.js" } } } ``` -Then import `shape`, `shape/runtime` (`init`, `HaskellError`) and-`shape/shape.wasm`.- ### `generate`: plain source files ```sh@@ -111,6 +104,25 @@ "$(wasm32-wasi-ghc --print-libdir)"/post-link.mjs -i shape.wasm -o out/ghc_wasm_jsffi.js wasm-opt -Oz shape.wasm -o out/shape.wasm ```++`shape.wasm` must end up next to `shape.js`.++## Loading++`shape.js` loads `shape.wasm` from its own directory with a top-level+`await`, so importing it is enough: the functions are ready to call. If the+`.wasm` cannot be loaded, the import fails.++This needs ES2022 modules (top-level `await`): current browsers, Node 20.16++or 22.3+, and bundlers targeting ES2022 or later. The `.wasm` is referenced as+`new URL("./shape.wasm", import.meta.url)`:++- webpack 5, Vite and Parcel copy it to the output on their own. With Vite,+ exclude the package from dependency pre-bundling+ (`optimizeDeps: { exclude: ["shape"] }`), which would break that reference+ in development.+- esbuild leaves the reference as is: copy `shape.wasm` next to the bundle+ (the [example](example/) does this). ## Installation
app/Main.hs view
@@ -21,7 +21,8 @@ , " (ghc-wasm-meta) on PATH." , "generate For plain source files: writes NameWasmBindings.hs and the Haskell" , " runtime to --hs-out, NAME.js, NAME.d.ts and the JS runtime to --js-out."- , " Compiling them is up to you."+ , " Compiling them is up to you; NAME.js loads NAME.wasm from its own"+ , " directory." , "" , " MODULE.hs module whose export list is the API (e.g. src/ShapeWasm.hs)" , " --name NAME package and file name (e.g. shape)"@@ -56,7 +57,7 @@ | null (getModule o) -> go rest (setModule f o) setModule getModule setName flags | otherwise -> Left ("unexpected argument " ++ f) name n- | n `elem` ["ghc_wasm_bindgen_runtime", "ghc_wasm_bindgen_internal", "ghc_wasm_jsffi"] = Left ("--name " ++ n ++ " would overwrite the runtime files")+ | n `elem` ["ghc_wasm_bindgen_internal", "ghc_wasm_jsffi"] = Left ("--name " ++ n ++ " would overwrite the runtime files") | null n || not (all (\c -> isAlphaNum c || c `elem` ("_-" :: String)) n) = Left ("--name must be letters, digits, _ or -: " ++ show n) | otherwise = Right n
ghc-wasm-bindgen.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: ghc-wasm-bindgen-version: 0.2.0.0+version: 0.3.0.0 synopsis: JavaScript/TypeScript bindings for GHC's WASM backend description: Generate JavaScript and TypeScript bindings for a pure Haskell module@@ -22,8 +22,6 @@ extra-doc-files: README.md extra-source-files:- runtime-js/ghc_wasm_bindgen_runtime.js- runtime-js/ghc_wasm_bindgen_runtime.d.ts runtime-js/ghc_wasm_bindgen_internal.js runtime-js/ghc_wasm_bindgen_internal.d.ts runtime-js/ghc_wasm_jsffi.d.ts
runtime-js/ghc_wasm_bindgen_internal.d.ts view
@@ -1,9 +1,10 @@ // ghc-wasm-bindgen runtime internals, imported by the generated bindings. -import type { Either } from "./ghc_wasm_bindgen_runtime";+/** `Either e a` from Haskell. */+export type Either<E, A> = { Left: E } | { Right: A }; -/** Instantiates the .wasm module; must resolve before any binding is called. */-export function init(source: string | URL | BufferSource): Promise<void>;+/** Instantiates the .wasm module; called by the generated module. */+export function load(url: URL): Promise<void>; /** A Haskell exception raised during a call. */ export class HaskellError extends Error {}
runtime-js/ghc_wasm_bindgen_internal.js view
@@ -3,21 +3,13 @@ import jsffiImports from "./ghc_wasm_jsffi.js"; let wasm;-let ready; -export function init(source) {- return (ready ??= load(source));-}--async function load(source) {- if (typeof source === "string" || source instanceof URL) {- const res = await fetch(source);- if (!res.ok) throw new Error(`ghc-wasm-bindgen: fetching ${source} failed (${res.status})`);- source = await res.arrayBuffer();- }+// Instantiates the .wasm module. Called once, by the generated module's+// top-level await, so the bindings are ready as soon as it is imported.+export async function load(url) { // jsffiImports needs the exports, which exist only after instantiation. const knot = {};- const { instance } = await WebAssembly.instantiate(source, {+ const { instance } = await WebAssembly.instantiate(await read(url), { ghc_wasm_jsffi: jsffiImports(knot), wasi_snapshot_preview1: wasi(knot), });@@ -26,6 +18,16 @@ wasm = knot; } +// Node's fetch does not read file: URLs. getBuiltinModule (rather than an+// import) keeps bundlers from trying to resolve node:fs for the browser.+async function read(url) {+ const fs = url.protocol === "file:" && globalThis.process?.getBuiltinModule?.("node:fs/promises");+ if (fs) return fs.readFile(url);+ const res = await fetch(url);+ if (!res.ok) throw new Error(`ghc-wasm-bindgen: fetching ${url} failed (${res.status})`);+ return res.arrayBuffer();+}+ // WASI subset the RTS uses for pure code; other calls return ENOSYS. function wasi(exports) { const view = () => new DataView(exports.memory.buffer);@@ -72,7 +74,6 @@ // __ghcWasmBindgenError is set by GhcWasmBindgen.Runtime.guarded. export function call(fn, ...args) {- if (!wasm) throw new Error("ghc-wasm-bindgen: await init() before calling the API"); const result = wasm[fn](...args); const err = wasm.__ghcWasmBindgenError; if (err !== undefined) {
− runtime-js/ghc_wasm_bindgen_runtime.d.ts
@@ -1,5 +0,0 @@-// Public interface of the ghc-wasm-bindgen runtime.--export { init, HaskellError } from "./ghc_wasm_bindgen_internal";--export type Either<E, A> = { Left: E } | { Right: A };
− runtime-js/ghc_wasm_bindgen_runtime.js
@@ -1,3 +0,0 @@-// Public interface of the ghc-wasm-bindgen runtime.--export { init, HaskellError } from "./ghc_wasm_bindgen_internal.js";
src/GhcWasmBindgen.hs view
@@ -27,6 +27,7 @@ import Data.FileEmbed (embedFile) import Data.List (isSuffixOf, stripPrefix) import Data.Maybe (mapMaybe)+import qualified Data.Text as T import qualified Data.Text.Encoding as TE import System.Directory import System.Exit (ExitCode (..))@@ -149,10 +150,8 @@ -- | The JS bindings and runtime, by file name. jsFiles :: String -> Api -> [(FilePath, ByteString)] jsFiles name api =- [ (name <.> "js", TE.encodeUtf8 (emitJs api))+ [ (name <.> "js", TE.encodeUtf8 (emitJs (T.pack name) api)) , (name <.> "d.ts", TE.encodeUtf8 (emitDts api))- , ("ghc_wasm_bindgen_runtime.js", $(embedFile "runtime-js/ghc_wasm_bindgen_runtime.js"))- , ("ghc_wasm_bindgen_runtime.d.ts", $(embedFile "runtime-js/ghc_wasm_bindgen_runtime.d.ts")) , ("ghc_wasm_bindgen_internal.js", $(embedFile "runtime-js/ghc_wasm_bindgen_internal.js")) , ("ghc_wasm_bindgen_internal.d.ts", $(embedFile "runtime-js/ghc_wasm_bindgen_internal.d.ts")) , ("ghc_wasm_jsffi.d.ts", $(embedFile "runtime-js/ghc_wasm_jsffi.d.ts"))
src/GhcWasmBindgen/EmitJavaScript.hs view
@@ -1,6 +1,6 @@--- | Emits the JS side of the bindings: @NAME.js@ (constructors, an--- encode/decode function per type, the API functions) and @NAME.d.ts@ (their--- TypeScript types).+-- | Emits the JS side of the bindings: @NAME.js@ (loading of @NAME.wasm@,+-- constructors, an encode/decode function per type, the API functions) and+-- @NAME.d.ts@ (their TypeScript types). -- -- The generated code never sees how values cross to Haskell: it converts -- them with the encode/decode functions of ghc_wasm_bindgen_internal.js, which@@ -16,19 +16,35 @@ header :: Api -> Text header api = "// Generated by ghc-wasm-bindgen from " <> apiModule api <> ". Do not edit." --- | @NAME.js@-emitJs :: Api -> Text-emitJs api =+-- | @NAME.js@. Loads @NAME.wasm@, found next to it, with a top-level await:+-- importing the module is enough to use it.+emitJs :: Text -> Api -> Text+emitJs name api = T.unlines $- [header api, "", "import * as $rt from \"./ghc_wasm_bindgen_internal.js\";", ""]+ [ header api+ , ""+ , "import * as $rt from \"./ghc_wasm_bindgen_internal.js\";"+ , ""+ , "export { HaskellError } from \"./ghc_wasm_bindgen_internal.js\";"+ , ""+ , "await $rt.load(new URL(" <> quote ("./" <> name <> ".wasm") <> ", import.meta.url));"+ , ""+ ] ++ concatMap jsType (apiTypes api) ++ intercalate [""] (map jsFunction (apiFns api)) --- | @NAME.d.ts@+-- | @NAME.d.ts@. Its imports name the @.js@ file, as @node16@/@nodenext@+-- module resolution requires (and @bundler@ accepts). emitDts :: Api -> Text emitDts api = T.unlines $- [header api, "", "import type * as $rt from \"./ghc_wasm_bindgen_runtime\";", ""]+ [ header api+ , ""+ , "import type * as $rt from \"./ghc_wasm_bindgen_internal.js\";"+ , ""+ , "export { HaskellError, type Either } from \"./ghc_wasm_bindgen_internal.js\";"+ , ""+ ] ++ concatMap dtsType (apiTypes api) ++ map dtsFunction (apiFns api)