Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: add webidl example #544

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 19 additions & 5 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,10 @@ jobs:
- name: string-reverse-upper
workspace: examples/components/string-reverse-upper
is-composed: true
- name: webidl-book-library
workspace: examples/components/webidl-book-library
requires-crates:
- webidl2wit-cli
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
Expand All @@ -290,7 +294,18 @@ jobs:
name: jco-build
path: obj

- name: install wac

- name: Install Rust
if: ${{ matrix.project.requires-crates != '[]' }}
run: rustup update stable --no-self-update && rustup default stable

- name: Install required rust crates
if: ${{ matrix.project.requires-crates != '[]' }}
uses: taiki-e/install-action@v2
with:
tool: ${{ join(matrix.project.requires-crates, ',') }}

- name: Install wac
if: ${{ matrix.project.is-composed }}
uses: jaxxstorm/action-install-gh-release@v1
with:
Expand All @@ -300,9 +315,8 @@ jobs:
rename-to: wac
chmod: 0755

- name: npm install
run: npm install
- run: npm install

- name: run all script for (${{ matrix.project.name }})
- name: Run all script for (${{ matrix.project.name }})
working-directory: ${{ matrix.project.dir }}
run: npm run all --workspace ${{ matrix.project.workspace }}
run: npm run all --workspace ${{ matrix.project.workspace }}
4 changes: 4 additions & 0 deletions examples/components/webidl-book-library/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules
dist
*.wasm
pnpm-lock.yaml
1 change: 1 addition & 0 deletions examples/components/webidl-book-library/.nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v22.5.1
54 changes: 54 additions & 0 deletions examples/components/webidl-book-library/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# `webidl-book-library`

This component showcases building WebAssembly components that line up with [WebIDL][webidl] interfaces by
using the experimental [built-in support for WebIDL in `jco`][jco-experimental-webidl].

We can accomplish this by:

- Writing WebIDL specifications
- Using [`webidl2wit`][webidl2wit] to turn WebIDL specifications into [WIT interfaces][wit]
- Using `jco componentize` to build Javascript WebAssembly components that target the relevant WIT interfaces
- Using `jco transpile` to compile that component to run in a JS context (like NodeJS)
- Writing host bindings (`demo.js`) that WebAssembly cna use

A common use case for WebIDL is targeting browsers, as the WebIDL is primarily used there
to document interfaces for the web platform. This example stops short of using the web platform
WebIDL in favor of showing a simpler modeled interace with WebIDL: a book library (see [`./book-library.webidl`](./book-library.webidl)).

[jco-experimental-webidl]: https://github.com/bytecodealliance/jco/blob/main/docs/src/transpiling.md#experimental-webidl-imports
[webidl]: https://en.wikipedia.org/wiki/Web_IDL
[wit]: https://github.com/WebAssembly/component-model/blob/main/design/mvp/WIT.md

## Dependencies

To run this example, you'll need the following tools

| Tool | Description | Install instructions |
|----------------------------|----------------------------------|-----------------------------------------|
| [`jco`][jco] | Javascript WebAssembly toolcahin | `npm install -g @bytecodealliance/jco` |
| [`webidl2wit`][webidl2wit] | Converts WebIDL to WIT | `cargo install --locked webidl2wit-cli` |

[jco]: https://github.com/bytecodealliance/jco
[webidl2wit]: https://github.com/wasi-gfx/webidl2wit

## Quickstart

Once dependencies are installed, you can use your favorite NodeJS package manager to run the steps:

```console
npm install
npm run generate:wit
npm run generate:types
npm run build
npm run transpile
```

> [!NOTE]
> To run all these steps at once, you can run `npm run all`

After running the component build, we can run the example code that uses our WebAssembly component,
and the WebIDL interface & implementation we made:

```
node demo.js
```
67 changes: 67 additions & 0 deletions examples/components/webidl-book-library/book-library.webidl
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// This WebIDL file will be used to automatically generate WIT,
// using the `webidl2wit` tool.
//
// see: https://github.com/MendyBerger/webidl2wit/tree/main

// WebIDL enums are converted as standard WIT enums
enum BookGenre {
"fiction",
"non-fiction",
"mystery",
"fantasy",
"science-fiction",
"biography"
};

// WebIDL typedefs are converted into WIT type aliases
typedef DOMString BookTitle;

// WebIDL dictionaries are turned into WIT structs
dictionary Book {
required BookTitle title;
required DOMString author;
BookGenre genre;
unsigned short pages;
};

// WebIDL interfaces become WIT resources
interface Library {
constructor();

readonly attribute unsigned long totalBooks;

// Add a Book
boolean addBook(Book book);

// Remove a book
boolean removeBook(DOMString title);

// Retrieve a book by title (if present)
Book? getBookByTitle(DOMString title);

// List all the books
sequence<Book>? listBooks();
};

interface AdvancedLibrary : Library {
FrozenArray<Book> filterBooks(DOMString name);
};

partial interface Library {
readonly attribute LibraryName libraryName;

// Rename this library
undefined renameLibrary(LibraryName newName);
};

typedef DOMString LibraryName;

// WebIDL interfaces become WIT resources
interface BookManager {
constructor();

readonly attribute Library library;

// Initialize a library
undefined initLibrary(LibraryName name);
};
125 changes: 125 additions & 0 deletions examples/components/webidl-book-library/demo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
////////////////////////////////
// WebIDL compliant interface //
////////////////////////////////

class Book {
constructor(title, author, genre, pages) {
this.title = title;
this.author = author;
this.genre = genre || null;
this.pages = pages || 0;
}
}

class Library {
constructor(name) {
this.libraryName = name;
this.books = new Map();
}

get totalBooks() {
return this.books.size;
}

addBook(book) {
if (this.books.has(book.title)) {
return false; // Book already exists
}
this.books.set(book.title, book);
return true;
}

removeBook(title) {
return this.books.delete(title);
}

getBookByTitle(title) {
return this.books.get(title) || null;
}

listBooks() {
return Array.from(this.books.values());
}

renameLibrary(newName) {
this.libraryName = newName;
}

asAdvancedLibrary() {
const al = new AdvancedLibrary();
al.libraryName = this.libraryName;
al.books = this.books;
return al;
}
}

class AdvancedLibrary extends Library {
constructor() {
super();
}

asLibrary() {
return new Library("advanced-library");
}

filterBooks(name) {
return Array.from(this.books.values()).filter((book) => {
return (
book.title.toLowerCase().includes(name.toLowerCase()) ||
book.author.toLowerCase().includes(name.toLowerCase())
);
});
}
}

class BookManager {
constructor() {
this._library = null;
}

initLibrary(name) {
const library = new Library(name);
this._library = library;
}

library() {
if (!this._library) {
throw new Error("initLibrary must be called first!");
}
const library = this._library;
this._library = null;
return library;
}
}

// Wire up the implementations above to globalThis
// which will be used by the component implicitly
//
// While somewhat awkward, current jco bindings
// generate a member/namespace for every dash, so:
//
// "global-book-library" => globalThis.book.library
// "global-console" => globalThis.console
globalThis.book = {
library: {
AdvancedLibrary,
Library,
BookManager,
},
};

////////////////////////////////
// Usage of transpiled module //
////////////////////////////////

// NOTE: we use a dynamic import of our transpiled WebAssembly component
// to ensure that globalThis is setup prior to the component logic running
const { librarian } = await import("./dist/transpiled/librarian.js");

let library = librarian.createLocalLibrary();

library = librarian.addFavoriteBook(library);

const [_library, favorites] = librarian.getFavoriteBooks(library);

console.log("Librarian's favorite books:", favorites);
31 changes: 31 additions & 0 deletions examples/components/webidl-book-library/librarian.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { BookManager } from "webidl:pkg/global-book-library";

export const librarian = {
createLocalLibrary() {
let bm = new BookManager();
bm.initLibrary("great-library-of-wasmxandria");
return bm.library();
},

addFavoriteBook(library) {
if (!library) {
throw new Error("missing/invalid library");
}
library.addBook({
title: "The Library Book",
author: "Susan Orlean",
genre: BookManager.NonFiction,
pages: 317,
});
return library;
},

getFavoriteBooks(library) {
if (!library) {
throw new Error("missing/invalid library");
}
let advanced = library.asAdvancedLibrary();
let favorites = advanced.filterBooks("library");
return [library, favorites];
},
};
15 changes: 15 additions & 0 deletions examples/components/webidl-book-library/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "webidl-book-library-wasm",
"description": "Example WebAssembly component showcasing WIT interfaces auto-generated from WebIDL",
"type": "module",
"scripts": {
"generate:wit": "webidl2wit --webidl-interface global-book-library -i book-library.webidl -o wit/deps/book-library.wit",
"generate:types": "jco types wit -o dist/generated/types",
"build": "jco componentize librarian.js --wit wit/ --world-name component --out librarian.wasm --disable all",
"transpile": "jco transpile librarian.wasm -o dist/transpiled",
"all": "npm run generate:wit && npm run generate:types && npm run build && npm run transpile && node demo.js"
},
"devDependencies": {
"@bytecodealliance/jco": "1.10.2"
}
}
23 changes: 23 additions & 0 deletions examples/components/webidl-book-library/wit/component.wit
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/// This package combines with of the auto-generated `book-library.wit` which
/// produces the package 'webidl:pkg' which contains the `global-book-library`
/// interface, which will be picked up and put on globalThis by jco.
package example:component;

interface librarian {
use webidl:pkg/global-book-library.{book, book-genre, library};

/// Create a local library
create-local-library: func() -> library;

/// Add this librarian's favorite book to the library
add-favorite-book: func(l: library) -> library;

/// Get a list of the favorite books
get-favorite-books: func(l: library) -> tuple<library, list<book>>;
}

world component {
import webidl:pkg/global-book-library;

export librarian;
}
Loading
Loading