├── examples
├── dom
│ ├── .gitignore
│ ├── Cargo.toml
│ ├── public
│ │ └── index.html
│ ├── Makefile
│ └── src
│ │ └── lib.rs
├── callbacks
│ ├── .gitignore
│ ├── Cargo.toml
│ ├── public
│ │ └── index.html
│ ├── Makefile
│ └── src
│ │ └── lib.rs
├── features
│ ├── .gitignore
│ ├── Cargo.toml
│ ├── public
│ │ └── index.html
│ ├── Makefile
│ └── src
│ │ ├── lib.rs
│ │ └── keycodes.rs
└── minimal
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
├── .gitignore
├── .github
├── assets
│ └── tinyweb-youtube.jpg
├── workflows
│ └── tests.yml
└── notes
│ └── runtime.md
├── Cargo.toml
├── src
├── rust
│ ├── Cargo.toml
│ ├── src
│ │ ├── lib.rs
│ │ ├── allocations.rs
│ │ ├── signals.rs
│ │ ├── router.rs
│ │ ├── callbacks.rs
│ │ ├── runtime.rs
│ │ ├── element.rs
│ │ └── invoke.rs
│ └── tests
│ │ └── mod.rs
└── js
│ ├── main.test.js
│ └── main.js
├── LICENSE
├── README.md
└── Cargo.lock
/examples/dom/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | target
3 | .DS_Store
4 |
--------------------------------------------------------------------------------
/examples/callbacks/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 |
--------------------------------------------------------------------------------
/examples/features/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 |
--------------------------------------------------------------------------------
/.github/assets/tinyweb-youtube.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LiveDuo/tinyweb/HEAD/.github/assets/tinyweb-youtube.jpg
--------------------------------------------------------------------------------
/examples/dom/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "dom"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | [lib]
7 | crate-type = ["cdylib"]
8 | test = false
9 |
10 | [dependencies]
11 | tinyweb = { path = "../../src/rust" }
12 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | resolver = "2"
3 | members = [
4 | "src/rust",
5 | "examples/minimal",
6 | "examples/features",
7 | "examples/dom",
8 | "examples/callbacks",
9 | ]
10 | default-members = ["src/rust"]
11 |
--------------------------------------------------------------------------------
/examples/callbacks/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "callbacks"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | [lib]
7 | crate-type = ["cdylib"]
8 | test = false
9 |
10 | [dependencies]
11 | tinyweb = { path = "../../src/rust" }
12 |
--------------------------------------------------------------------------------
/examples/minimal/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "minimal"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | [lib]
7 | crate-type =["cdylib"]
8 | test = false
9 |
10 | [dependencies]
11 | tinyweb = { path = "../../src/rust" }
12 |
13 |
--------------------------------------------------------------------------------
/examples/features/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "features"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | [lib]
7 | crate-type =["cdylib"]
8 | test = false
9 |
10 | [dependencies]
11 | tinyweb = { path = "../../src/rust" }
12 | json = "0.12.4"
13 |
14 |
--------------------------------------------------------------------------------
/examples/dom/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/examples/minimal/src/lib.rs:
--------------------------------------------------------------------------------
1 |
2 | use tinyweb::invoke::*;
3 |
4 | #[no_mangle]
5 | pub fn main() {
6 | let body = Js::invoke("return document.querySelector({})", &["body".into()]).to_ref().unwrap();
7 | Js::invoke("{}.innerHTML = {}", &[body.into(), "hello".into()]);
8 | }
9 |
--------------------------------------------------------------------------------
/examples/callbacks/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/rust/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "tinyweb"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | [lib]
7 | doc = false
8 | doctest = false
9 |
10 | [dev-dependencies]
11 | fantoccini = "0.21.1"
12 | serde_json = "1.0.127"
13 | tokio = { version = "1", features = ["full"] }
14 |
--------------------------------------------------------------------------------
/examples/dom/Makefile:
--------------------------------------------------------------------------------
1 | build:
2 | cargo build --target wasm32-unknown-unknown -r
3 | mkdir -p /tmp/public
4 | cp ../../target/wasm32-unknown-unknown/release/dom.wasm /tmp/public/dom.wasm
5 | cp ../../src/js/main.js /tmp/public/main.js
6 | cp public/index.html /tmp/public/index.html
7 | start:
8 | python3 -m http.server -d /tmp/public
9 | dev:
10 | make build
11 | make start
12 |
--------------------------------------------------------------------------------
/examples/features/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/examples/features/Makefile:
--------------------------------------------------------------------------------
1 | build:
2 | cargo build --target wasm32-unknown-unknown -r
3 | mkdir -p /tmp/public
4 | cp ../../target/wasm32-unknown-unknown/release/features.wasm /tmp/public/features.wasm
5 | cp ../../src/js/main.js /tmp/public/main.js
6 | cp public/index.html /tmp/public/index.html
7 | start:
8 | python3 -m http.server -d /tmp/public
9 | dev:
10 | make build
11 | make start
12 |
--------------------------------------------------------------------------------
/examples/callbacks/Makefile:
--------------------------------------------------------------------------------
1 | build:
2 | cargo build --target wasm32-unknown-unknown -r
3 | mkdir -p /tmp/public
4 | cp ../../target/wasm32-unknown-unknown/release/callbacks.wasm /tmp/public/callbacks.wasm
5 | cp ../../src/js/main.js /tmp/public/main.js
6 | cp public/index.html /tmp/public/index.html
7 | start:
8 | python3 -m http.server -d /tmp/public
9 | dev:
10 | make build
11 | make start
12 |
--------------------------------------------------------------------------------
/examples/dom/src/lib.rs:
--------------------------------------------------------------------------------
1 |
2 | use tinyweb::callbacks::create_callback;
3 | use tinyweb::invoke::*;
4 |
5 | #[no_mangle]
6 | pub fn main() {
7 |
8 | std::panic::set_hook(Box::new(|e| { Js::invoke("console.log({})", &[e.to_string().into()]); }));
9 |
10 | let button = Js::invoke("return document.createElement('button')", &[]).to_ref().unwrap();
11 | Js::invoke("{}.textContent = 'Click'", &[button.into()]);
12 |
13 | let function_ref = create_callback(move |e| { Js::invoke("alert('hello')", &[]); Js::deallocate(e); });
14 | Js::invoke("{}.addEventListener('click',{})", &[button.into(), function_ref.into()]);
15 |
16 | let body = Js::invoke("return document.querySelector('body')", &[]).to_ref().unwrap();
17 | Js::invoke("{}.appendChild({})", &[body.into(), button.into()]);
18 | }
19 |
--------------------------------------------------------------------------------
/src/rust/src/lib.rs:
--------------------------------------------------------------------------------
1 |
2 | pub mod callbacks;
3 | pub mod allocations;
4 | pub mod runtime;
5 | pub mod invoke;
6 |
7 | pub mod signals;
8 | pub mod element;
9 | pub mod router;
10 |
11 | // Use: crate::println!("{}", 42);
12 | #[macro_export]
13 | macro_rules! println {
14 | ($fmt:expr) => { Js::invoke("console.log({})", &[format!($fmt).into()]); };
15 | ($fmt:expr, $($arg:tt)*) => { Js::invoke("console.log({})", &[format!($fmt, $($arg)*).into()]); };
16 | }
17 |
18 | // Web browser specification
19 | // https://github.com/w3c/webref
20 |
21 | // Count LOC (excluding tests)
22 | // ```
23 | // git ls-files ':(glob)src/rust/src/**' | xargs cat | sed '/#\[test\]/,/}/d' | wc -l
24 | // ```
25 |
26 | // List files
27 | // ```
28 | // git ls-files ':(glob)src/rust/src/**' | xargs wc -l | sort -r
29 | // ```
30 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 |
2 | name: Tests
3 |
4 | on:
5 | push:
6 | branches: [ "master" ]
7 |
8 | env:
9 | RUST_BACKTRACE: short
10 | SKIP_GUEST_BUILD: 1
11 | CARGO_INCREMENTAL: 0
12 | CARGO_NET_RETRY: 10
13 | CI: 1
14 |
15 | jobs:
16 | build:
17 | runs-on: ubuntu-latest
18 | timeout-minutes: 10
19 | steps:
20 |
21 | # setup geckodriver
22 | - uses: browser-actions/setup-geckodriver@latest
23 |
24 | # setup git
25 | - uses: actions/checkout@v3
26 | - uses: Swatinem/rust-cache@v2
27 |
28 | # setup rust
29 | - run: export PATH=~/.cargo/bin:/usr/local/bin/:$PATH
30 | - run: rustup target add wasm32-unknown-unknown
31 |
32 | # run rust tests
33 | - run: cargo test
34 |
35 | # run js tests
36 | - run: node src/js/main.test.js
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Andreas Tzionis
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/examples/callbacks/src/lib.rs:
--------------------------------------------------------------------------------
1 |
2 | use tinyweb::callbacks::{create_async_callback, create_callback};
3 | use tinyweb::runtime::Runtime;
4 | use tinyweb::invoke::*;
5 |
6 | #[no_mangle]
7 | pub fn main() {
8 |
9 | std::panic::set_hook(Box::new(|e| { Js::invoke("console.log({})", &[e.to_string().into()]); }));
10 |
11 | // invoke
12 | Js::invoke("console.log('invoke')", &[]);
13 |
14 | // invoke callback
15 | let function_ref = create_callback(move |_s| { Js::invoke("console.log('invoke timer')", &[]); });
16 | Js::invoke("setTimeout({}, 1000)", &[function_ref.into()]);
17 |
18 | // invoke async callback
19 | let url = "https://pokeapi.co/api/v2/pokemon/1";
20 | let (callback_ref, future) = create_async_callback();
21 | Js::invoke("fetch({}).then(r => r.json()).then(r => { {}(r) })", &[url.into(), callback_ref.into()]);
22 | Runtime::block_on(async move {
23 | let future_leak = Box::leak(Box::new(future));
24 | let object_ref = future_leak.await;
25 | let result = Js::invoke("return {}.name", &[object_ref.into()]).to_str().unwrap();
26 | Js::invoke("console.log('invoke fetch', {})", &[result.into()]);
27 | Js::deallocate(object_ref);
28 | });
29 | }
30 |
--------------------------------------------------------------------------------
/src/rust/src/allocations.rs:
--------------------------------------------------------------------------------
1 |
2 | use std::cell::RefCell;
3 |
4 | thread_local! {
5 | pub static ALLOCATIONS: RefCell>> = RefCell::new(Vec::new());
6 | }
7 |
8 | #[no_mangle]
9 | pub fn create_allocation(size: usize) -> usize {
10 | ALLOCATIONS.with_borrow_mut(|s| { s.push(vec![0; size]); s.len() - 1 })
11 | }
12 |
13 | #[no_mangle]
14 | pub fn get_allocation(allocation_id: usize) -> *const u8 {
15 | ALLOCATIONS.with_borrow(|s| s.get(allocation_id).unwrap().as_ptr())
16 | }
17 |
18 | #[cfg(test)]
19 | mod tests {
20 | use super::*;
21 |
22 | #[test]
23 | fn test_allocation() {
24 |
25 | // test string
26 | let text = "hello";
27 | let id = create_allocation(1);
28 | ALLOCATIONS.with_borrow_mut(|s| { s[id as usize] = text.as_bytes().to_vec(); });
29 | let allocation_data = ALLOCATIONS.with_borrow(|s| s.get(id as usize).unwrap().to_owned());
30 | let memory_text = String::from_utf8(allocation_data).unwrap();
31 | assert_eq!(memory_text, text);
32 |
33 | // test vec
34 | let vec = vec![1, 2];
35 | let id = create_allocation(1);
36 | ALLOCATIONS.with_borrow_mut(|s| { s[id as usize] = vec.clone(); });
37 | let memory_vec = ALLOCATIONS.with_borrow(|s| s.get(id as usize).unwrap().to_owned());
38 | assert_eq!(memory_vec, vec);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/.github/notes/runtime.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ### Runtime flow
4 |
5 | ```js
6 | // 1. Register a callback and invoke `fetch` that triggers the callback when is finishes
7 | [Log] create_async_callback future_id=0 -> [Log] create_callback id=0 && [Log] js_invoke `fetch` id=0
8 |
9 | // 2. Use the `block_on` method and `await` the future inside
10 | // When the future is awaited it calls `poll` function that sets `FutureState` to `Pending(waker)`
11 | [Log] runtime block on -> [Log] future poll -> [Log] poll future pending
12 |
13 | // 3. When the `fetch` callback is triggered, schedule a `setTimeout(0)` callback that calls future poll
14 | [Log] handle_callback id=0 -> [Log] waker wake -> [Log] create_callback id=2 && [Log] js_invoke `setTimeout(0)` id=0
15 |
16 | // 4. When the `setTimeout(0)` callback is triggered, resolve future to `Poll::Ready(T)`
17 | [Log] handle_callback id=0 -> [Log] future poll -> [Log] poll future completed
18 | ```
19 |
20 |
21 | ### Implementation quirks
22 |
23 | 1. Updating `FutureState` in 2 different places
24 | - Notes: It's updated in `create_async_callback` and in the `Future` trait impl
25 | - Explanation: `create_async_callback` has the `result` value and `Future` has access to the concrete `self` type
26 |
27 | 2. Calling `poll` in `wake_fn` through a Javascript callback instead of directly calling
28 | - Notes: The `wake_fn` function schedules a callback with `setTimeout(0)` that does `Runtime::poll(&future)`
29 | - Explanation: `Runtime::poll` has a mutable borrow that still holds when `wake_fn` tries to borrow again
30 |
--------------------------------------------------------------------------------
/src/rust/src/signals.rs:
--------------------------------------------------------------------------------
1 |
2 | use std::{cell::RefCell, rc::Rc};
3 |
4 | #[derive(Clone)]
5 | pub struct Signal {
6 | value: Rc>,
7 | // NOTE: since `FnMut` can mutate state it has to go behind a smart pointer
8 | subscribers: Rc>>>>
9 | }
10 |
11 | impl Signal {
12 | pub fn new(value: T) -> &'static Self {
13 | let signal = Self { value: Rc::new(RefCell::new(value)), subscribers: Default::default(), };
14 | &*Box::leak(Box::new(signal))
15 | }
16 | pub fn get(&self) -> T {
17 | self.value.borrow().clone()
18 | }
19 | pub fn set(&self, new_value: T) {
20 | // store value
21 | *self.value.borrow_mut() = new_value;
22 |
23 | // trigger effects
24 | self.subscribers.borrow_mut().iter().for_each(|f| { f.borrow_mut()(); });
25 | }
26 | pub fn on(&self, mut cb: impl FnMut(T) + 'static) {
27 |
28 | // get callback
29 | let signal_clone = self.clone();
30 | let cb_ref = Rc::new(RefCell::new(move || { cb(signal_clone.get()); }));
31 |
32 | // store callback
33 | self.subscribers.borrow_mut().push(cb_ref.to_owned());
34 |
35 | // trigger once
36 | cb_ref.borrow_mut()();
37 | }
38 | }
39 |
40 | #[cfg(test)]
41 | mod tests {
42 |
43 | use super::*;
44 |
45 | #[test]
46 | fn test_signals() {
47 |
48 | // create signal
49 | let logs: Rc>> = Default::default();
50 | let signal = Signal::new(10);
51 |
52 | // create effects
53 | let logs_clone = logs.clone();
54 | signal.on(move |v| { logs_clone.borrow_mut().push(v); });
55 | let logs_clone = logs.clone();
56 | signal.on(move |v| { logs_clone.borrow_mut().push(v); });
57 |
58 | // update signal
59 | signal.set(20);
60 | signal.set(30);
61 |
62 | // check logs
63 | assert_eq!(*logs.borrow(), vec![10, 10, 20, 20, 30, 30]);
64 | }
65 |
66 | }
67 |
--------------------------------------------------------------------------------
/src/js/main.test.js:
--------------------------------------------------------------------------------
1 | const test = require('node:test')
2 | const assert = require('node:assert')
3 |
4 | const { readParamsFromMemory, writeBufferToMemory, wasmModule } = require('./main')
5 |
6 | // node src/js/main.test.js
7 |
8 | test('check read params', () => {
9 |
10 | const float64View = new DataView(new ArrayBuffer(8))
11 | float64View.setFloat64(0, 42.42, true)
12 | const float64Array = new Uint8Array(float64View.buffer)
13 |
14 | const bigInt64View = new DataView(new ArrayBuffer(8))
15 | bigInt64View.setBigInt64(0, 42n, true)
16 | const bigInt64Array = new Uint8Array(bigInt64View.buffer)
17 |
18 | const uint32View = new DataView(new ArrayBuffer(4))
19 | uint32View.setUint32(0, 42, true)
20 | const uint32Array = new Uint8Array(uint32View.buffer)
21 |
22 | const testCases = [
23 | {memory: [0], expected: [undefined]},
24 | {memory: [1], expected: [null]},
25 | {memory: [2, ...float64Array], expected: [42.42]},
26 | {memory: [3, ...bigInt64Array], expected: [42n]},
27 | {memory: [4, ...uint32Array, ...uint32Array], expected: ['']},
28 | {memory: [5], expected: [true]},
29 | {memory: [6], expected: [false]},
30 | {memory: [7, ...uint32Array], expected: [undefined]},
31 | ]
32 | for (const testCase of testCases) {
33 | wasmModule.instance = { exports: { memory: { buffer: testCase.memory } } }
34 |
35 | const result = readParamsFromMemory(0, testCase.memory.length)
36 | assert.deepStrictEqual(result, testCase.expected)
37 | }
38 | })
39 |
40 | test('check write buffer', () => {
41 |
42 | const testCases = [
43 | {memory: [], expected: 0},
44 | ]
45 | const create_allocation = () => { return 0 }
46 | const get_allocation = () => { return 0 }
47 | for (const testCase of testCases) {
48 |
49 | const exports = { create_allocation, get_allocation, memory: { buffer: testCase.memory } }
50 | wasmModule.instance = { exports }
51 |
52 | const result = writeBufferToMemory(0, [])
53 | assert.deepStrictEqual(result, testCase.expected)
54 | }
55 | })
56 |
--------------------------------------------------------------------------------
/src/rust/src/router.rs:
--------------------------------------------------------------------------------
1 |
2 |
3 | use std::collections::HashMap;
4 |
5 | use crate::invoke::{Js, ObjectRef};
6 | use crate::element::El;
7 |
8 | #[derive(Debug, Clone)]
9 | pub struct Page { pub path: String, pub element: El, pub title: Option }
10 |
11 | impl Page {
12 | pub fn new(path: &str, element: El) -> Self {
13 | Self { path: path.to_owned(), element, title: None }
14 | }
15 | pub fn title(mut self, title: String) -> Self {
16 | self.title = Some(title);
17 | self
18 | }
19 | }
20 |
21 | #[derive(Debug, Default)]
22 | pub struct Router { pub root: Option, pub pages: HashMap:: }
23 |
24 | impl Router {
25 | pub fn new(root: &str, pages: &[Page]) -> Self {
26 | let body = Js::invoke("return document.querySelector({})", &[root.into()]).to_ref().unwrap();
27 | let pathname = Js::invoke("return window.location.pathname", &[]).to_str().unwrap();
28 | let page = pages.iter().find(|&s| *s.path == pathname).unwrap_or(&pages[0]);
29 | page.element.mount(&body);
30 |
31 | let mut default_page = pages.first().cloned().unwrap();
32 | default_page.path = "/".to_owned();
33 |
34 | let mut pages = pages.iter().map(|p| (p.path.clone(), p.to_owned())).collect::>();
35 | pages.push((default_page.path.clone(), default_page.to_owned()));
36 | Self { pages: HashMap::from_iter(pages), root: Some(body) }
37 | }
38 | pub fn navigate(&self, route: &str) {
39 |
40 | // unmount page
41 | let pathname = Js::invoke("return window.location.pathname", &[]).to_str().unwrap();
42 | let (_, current_page) = self.pages.iter().find(|&(s, _)| *s == pathname).unwrap();
43 | current_page.element.unmount();
44 |
45 | // set html
46 | let body = self.root.as_ref().unwrap();
47 | Js::invoke("{}.innerHTML = {}", &[body.into(), "".into()]);
48 |
49 | // mount new page
50 | let page = self.pages.get(route).unwrap();
51 | page.element.mount(&body);
52 |
53 | // push state
54 | let page_str = page.title.to_owned().unwrap_or_default();
55 | Js::invoke("window.history.pushState({ },{},{})", &[page_str.into(), route.into()]);
56 |
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/rust/tests/mod.rs:
--------------------------------------------------------------------------------
1 |
2 | use std::process::{Command, Stdio};
3 | use std::sync::{Arc, Mutex};
4 | use std::time::Duration;
5 | use std::env::temp_dir;
6 | use std::path::PathBuf;
7 |
8 | use fantoccini::wd::Capabilities;
9 | use fantoccini::{ClientBuilder, Locator};
10 |
11 | pub const WASM_TRIPLET: &str = "wasm32-unknown-unknown";
12 |
13 | fn get_pid_on_port(port: u16) -> Option {
14 | let output = Command::new("lsof").args(&["-ti", format!(":{port}").as_str()]).output().unwrap();
15 | let stdout_opt = if output.stdout.is_empty() { None } else { Some(output.stdout) };
16 | stdout_opt.map(|o| std::str::from_utf8(&o).map(|p| p.trim().parse().unwrap()).unwrap())
17 | }
18 |
19 | fn setup_temp_project() -> PathBuf {
20 | // get paths
21 | let temp_dir = temp_dir();
22 | let cwd = std::env::current_dir().unwrap();
23 | let project_path = cwd.parent().unwrap().parent().unwrap();
24 |
25 | // build wasm
26 | let p = Command::new("cargo").args(["build", "-p", "minimal", "--target", WASM_TRIPLET]).output().unwrap();
27 | assert!(p.status.success());
28 |
29 | // copy wasm
30 | let wasm_path = project_path.join("target").join(WASM_TRIPLET).join("debug").join("minimal.wasm");
31 | std::fs::copy(wasm_path, temp_dir.join("client.wasm")).unwrap();
32 |
33 | // copy js
34 | let js_path = project_path.join("src").join("js").join("main.js");
35 | std::fs::copy(js_path, temp_dir.join("main.js")).unwrap();
36 |
37 | // copy html
38 | let html = r#""#;
39 | std::fs::write(temp_dir.join("index.html"), html).unwrap();
40 |
41 | temp_dir
42 | }
43 |
44 | // lsof -i tcp:4444 && kill -9 ${PID}
45 | #[tokio::test]
46 | async fn test_wasm() -> Result<(), fantoccini::error::CmdError> {
47 |
48 | // start daemon
49 | let lock = Arc::new(Mutex::new(None));
50 | let lock_clone = lock.clone();
51 | std::thread::spawn(move || {
52 |
53 | let pid = get_pid_on_port(4444);
54 | if let Some(pid) = pid {
55 | Command::new("kill").arg(format!("{}", pid)).status().unwrap();
56 | }
57 |
58 | let child = Command::new("geckodriver").stderr(Stdio::null()).spawn().unwrap();
59 | lock_clone.lock().map(|mut s| { *s = Some(child); }).unwrap();
60 | });
61 | std::thread::sleep(Duration::from_millis(1_000));
62 |
63 | // open browser
64 | let mut client_builder = ClientBuilder::native();
65 | let mut caps = Capabilities::new();
66 | caps.insert("moz:firefoxOptions".to_string(), serde_json::json!({ "args": ["--headless"] }));
67 | client_builder.capabilities(caps);
68 | let client = client_builder.connect("http://localhost:4444").await.unwrap();
69 |
70 | // prepare project
71 | let project_dir = setup_temp_project();
72 |
73 | // load html
74 | let index_html = "/index.html";
75 | let url = format!("file://{}{}", project_dir.to_str().unwrap(), index_html);
76 | client.goto(&url).await?;
77 |
78 | std::thread::sleep(Duration::from_millis(1_000));
79 |
80 | // check body
81 | let body = client.find(Locator::Css("body")).await?;
82 | let body_str = body.html(true).await?;
83 | assert!(body_str.contains("hello"));
84 |
85 | // stop browser
86 | client.close().await?;
87 |
88 | // stop daemon
89 | lock.lock().map(|mut s| {
90 | let child = s.as_mut().unwrap();
91 | child.kill().unwrap();
92 | }).unwrap();
93 |
94 | Ok(())
95 |
96 | }
--------------------------------------------------------------------------------
/src/rust/src/callbacks.rs:
--------------------------------------------------------------------------------
1 |
2 | use crate::runtime::{FutureState, FutureTask};
3 | use crate::invoke::{Js, ObjectRef};
4 |
5 | use std::collections::HashMap;
6 | use std::cell::RefCell;
7 | use std::rc::Rc;
8 |
9 | thread_local! {
10 | pub static CALLBACK_HANDLERS: RefCell>> = Default::default();
11 | }
12 |
13 | pub fn create_callback(mut handler: impl FnMut(ObjectRef) + 'static) -> ObjectRef {
14 | let code = r#"
15 | const handler = (e) => {
16 | const handlerObjectId = getRandomId();
17 | objects.set(handlerObjectId, e);
18 | wasmModule.instance.exports.handle_callback(objectId, handlerObjectId);
19 | };
20 | const objectId = getRandomId();
21 | objects.set(objectId, handler);
22 | return objectId;
23 | "#;
24 | let object_id = Js::invoke(code, &[]).to_num().unwrap();
25 | let function_ref = ObjectRef::new(object_id as u32);
26 | let cb = move |value| { handler(value); };
27 | CALLBACK_HANDLERS.with(|s| { s.borrow_mut().insert(function_ref.clone(), Box::new(cb)); });
28 | function_ref
29 | }
30 |
31 | #[no_mangle]
32 | pub fn handle_callback(callback_id: u32, param_id: u32) {
33 |
34 | let object_ref = ObjectRef::new(param_id as u32);
35 | let callback_ref = ObjectRef::new(callback_id);
36 |
37 | CALLBACK_HANDLERS.with(|s| {
38 | let handler = s.borrow_mut().get_mut(&callback_ref).unwrap() as *mut Box;
39 | unsafe { (*handler)(object_ref) }
40 | });
41 |
42 | Js::deallocate(callback_ref);
43 | }
44 |
45 | pub fn create_async_callback() -> (ObjectRef, FutureTask) {
46 | let future = FutureTask { state: Rc::new(RefCell::new(FutureState::Init)) };
47 | let state_clone = future.state.clone();
48 | let callback_ref = create_callback(move |e| {
49 | let mut future_state = state_clone.borrow_mut();
50 | if let FutureState::Pending(ref mut waker) = &mut *future_state { waker.to_owned().wake(); }
51 | *future_state = FutureState::Ready(e);
52 | });
53 | return (callback_ref, future);
54 | }
55 |
56 | #[cfg(test)]
57 | mod tests {
58 |
59 | use std::cell::RefCell;
60 | use std::rc::Rc;
61 |
62 | use super::*;
63 |
64 | #[test]
65 | fn test_callback() {
66 |
67 | // add listener
68 | let has_run = Rc::new(RefCell::new(false));
69 | let has_run_clone = has_run.clone();
70 | create_callback(move |_| { *has_run_clone.borrow_mut() = true; });
71 |
72 | // simulate callback
73 | let function_ref = ObjectRef::new(0);
74 | handle_callback(*function_ref, 0);
75 | assert_eq!(*has_run.borrow(), true);
76 |
77 | // remove listener
78 | CALLBACK_HANDLERS.with(|s| { s.borrow_mut().remove(&function_ref); });
79 | let count = CALLBACK_HANDLERS.with(|s| s.borrow().len());
80 | assert_eq!(count, 0);
81 | }
82 |
83 | #[test]
84 | fn test_future_callback() {
85 |
86 | // add listener
87 | let (function_ref, future) = create_async_callback();
88 |
89 | // simulate callback
90 | handle_callback(*function_ref, 0);
91 | crate::runtime::Runtime::block_on(async move { future.await; });
92 |
93 | // remove listener
94 | CALLBACK_HANDLERS.with(|s| { s.borrow_mut().remove(&function_ref); });
95 | let count = CALLBACK_HANDLERS.with(|s| s.borrow().len());
96 | assert_eq!(count, 0);
97 | }
98 |
99 | }
100 |
--------------------------------------------------------------------------------
/src/rust/src/runtime.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | cell::RefCell,
3 | future::Future,
4 | mem::ManuallyDrop,
5 | pin::Pin,
6 | rc::Rc,
7 | task::{Context, Poll, RawWaker, RawWakerVTable, Waker}
8 | };
9 |
10 | use crate::callbacks::{create_async_callback, create_callback};
11 | use crate::invoke::{Js, JsValue, ObjectRef};
12 |
13 | pub enum FutureState { Init, Pending(Waker), Ready(ObjectRef) }
14 | pub struct FutureTask { pub state: Rc> }
15 |
16 | pub struct Runtime {}
17 |
18 | type FutureRc = Rc>>>>;
19 |
20 | impl Future for FutureTask {
21 | type Output = ObjectRef;
22 |
23 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll {
24 |
25 | let mut future_state = self.state.borrow_mut();
26 | match &*future_state {
27 | FutureState::Ready(result) => {
28 | Poll::Ready(result.to_owned())
29 | },
30 | _ => {
31 | *future_state = FutureState::Pending(cx.waker().to_owned());
32 | Poll::Pending
33 | }
34 | }
35 | }
36 | }
37 |
38 | impl Drop for FutureTask {
39 | fn drop(&mut self) {
40 | match *self.state.borrow_mut() {
41 | FutureState::Ready(id) => Js::deallocate(id),
42 | _ => {}
43 | }
44 | }
45 | }
46 |
47 | impl Runtime {
48 |
49 | fn poll(future_rc: &FutureRc) {
50 | let waker = Self::waker(&future_rc);
51 | let waker_forget = ManuallyDrop::new(waker);
52 | let context = &mut Context::from_waker(&waker_forget);
53 | let _poll = future_rc.borrow_mut().as_mut().poll(context);
54 | }
55 |
56 | // https://rust-lang.github.io/async-book/02_execution/03_wakeups.html
57 | fn waker(future_rc: &FutureRc) -> Waker {
58 |
59 | fn clone_fn(ptr: *const ()) -> RawWaker {
60 | let future = unsafe { FutureRc::::from_raw(ptr as *const _) };
61 | let _ = ManuallyDrop::new(future).clone();
62 | RawWaker::new(ptr, waker_vtable::())
63 | }
64 | fn wake_fn(ptr: *const ()) {
65 | let future = unsafe { FutureRc::::from_raw(ptr as *const _) };
66 | let function_ref = create_callback(move |e| { Runtime::poll(&future); Js::deallocate(e); });
67 | Js::invoke("window.setTimeout({},0)", &[function_ref.into()]);
68 | }
69 | fn drop_fn(ptr: *const ()) {
70 | let future = unsafe { FutureRc::::from_raw(ptr as *const _) };
71 | drop(future);
72 | }
73 | fn waker_vtable() -> &'static RawWakerVTable {
74 | &RawWakerVTable::new(clone_fn::, wake_fn::, wake_fn::, drop_fn::)
75 | }
76 | let waker = RawWaker::new(&**future_rc as *const _ as *const (), waker_vtable::());
77 | unsafe { Waker::from_raw(waker) }
78 | }
79 |
80 | pub fn block_on(future: impl Future