@zorse/drop v3.4.0
drop
Drop is a small toolbox of common tools in WebAssembly to provide a POSIX-like environment independent of the host system and platform capabilities.
Drop is designed primarily as a toolbox for file IO and offline scripting.
Zorse, TypeScript, TypeScriptX (React), and JavaScript syntax are supported.
$ npm install --global @zorse/drop
$ drop node test.ts
Hello From WebAssembly!
$ drop ls
webpack.std.mjs    src                lib                LICENSE
webpack.all.mjs    package.json       docs               Cargo.toml
tsconfig.json      package-lock.json  dist               Cargo.lock
test.zx.mjs        out                build.zx.mjs       CREDITS.md
test.ts            node_modules       build.rs           CMakeLists.txt
target             modules.tar.gz     build
std                modules            README.md
$ drop zip archive.zip file.txtGoals
- Isomorphism: Supporting runtime execution everywhere under a single API.
- Portability: Staying entirely in WebAssembly and WASI for max portability.
- Familiarity: Not introducing any new concepts. Faithfulness to NodeJS APIs.
Development
API Documentation: docs
To run the formatter and apply the style: npm run format -- --write.
To run the linter and apply the rules: npm run lint -- --apply.
To run the tests: npm test.
To build you need:
Take a look at the Workflow file, it's the most up to date way to build the project and it's pretty minimal/easy to follow locally.
If you have
emsdkinstalled at~/emsdk, executingnpm run build:devwill use itsemsdk_env.shwithout messing with your global shell.
Features
Drop is currently capable of running IstanbulJS and Babel internally.
Drop's JS runtime uses QuickJS and supports both CommonJS and ES Modules with a
proper global require function and a module object, just like NodeJS.
Drop's JS runtime uses SWC to transpile TypeScript(X) to JavaScript on the fly.
Drop currently offers the following features in WebAssembly:
- NodeJS with emulated native modules:- assert•- buffer: •- crypto: •- events: •- fs•- os•- url•- util•- path•- process•- punycode•- querystring•- readline•- stream•- stringdecoder•- timers•- tty•- zlib•- uvu(test harness)
 
- POSIX with emulated tooling:- awk•- base64•- basename•- cat•- chmod•- chown•- cksum•- clear•- cp•- date•- diff•- dirname•- echo•- egrep•- env•- expr•- false•- fgrep•- find•- grep•- hd•- head•- hexdump•- link•- ln•- ls•- md5sum•- mkdir•- mkfifo•- mknod•- mktemp•- mv•- nanozip•- patch•- printenv•- printf•- pwd•- readlink•- realpath•- rev•- rm•- rmdir•- sed•- sha256sum•- sleep•- sort•- split•- stat•- tail•- tar•- test•- touch•- true•- uniq•- unlink•- unzip•- usleep•- whoami•- xargs
 
Goal is to eventually go beyond Node and web browsers and consume these features cross-platform and polyglot.
Architecture
BusyBox is pretty simple to understand. This project compiles a BusyBox subset
to WebAssembly with Emscripten. This enables the code to offer usual POSIX tools
such as ls, cat, grep, sed, tar, zip, etc cross-platform.
Drop is written in Rust and depends on WASI for file IO and Clock access only.
Drop does NodeJS emulation and is meant to be kept so it's easy to port to any
other WASI host with minimal effort.
Drop uses a dual-loop model to run asynchronous logic outside its WASM guest.
The external loop is expected to offer extended native access such as fetch.Drop does not depend on wasm-bindgen or any other WASI proposals.
Apart from WASI for file access and Clock, Drop expects the following imports
to be available for the guest WASM module:
- drop.host_loop_push(buf: *const u8, len: usize);
- drop.host_loop_poll() -> usize;
And exposes the following functions to the host environment:
- drop_loop_push(buf: *const u8, len: usize);
- drop_loop_poll(); -> usize
In the dual-loop model, Asyncify
is used to mark host_loop_push as an asynchronous function, so that once it's
called by the guest WASM module, guest blocks until host processes the external
loop and once it returns to the WASM side, drop_loop_push is used to retrieve
the result of the external loop's poll. Calling to host_loop_poll is expected
to trigger drop_loop_push synchronously, if there's any data available.drop_loop_poll is called in a loop until it exits and is done processing.xxxx_loop_poll returns an "exit hash" which is an unsigned number, that can be
decoded to get the exit status and the exit code of either loops.  
Hashed exit code allows either loops to cancel each other. Format of this hashed
version is: [\d+][10]. If the first bit (rightmost digit) is 1, it indicates
the loop has exited and no longer should be polled. In this scenario, remaining
digits left-right are the exit code. For example 261 means the loop has exited
with exit code 26. If the first bit is 0, it indicates the loop is still in
progress and should be polled again. In this scenario, remaining digits left to
right are the number of remaining jobs in the loop. For example 120 means the
loop has 12 jobs remaining in its queue. 0 means a successful exit.
Format of the exchanged buf is JSON.
The dual-loop's execution model is shown in the following Mermaid diagram:
graph LR
  S((wasi.start)) --> A
  A[drop_loop_poll] --> |sync| B[host_loop_push]
  D[host_loop_poll]  --> |sync| C[drop_loop_push]
  B --> |async| Eg
  C --> |async| Eh
  Eg --> |no| D
  Eh --> |no| A
  Eg --> |yes| E
  Eh --> |yes| E
  Eg{guest exited?}
  Eh{host exited?}
  E((process.exit))