diff --git a/docz/data/engines.xls b/docz/data/engines.xls
index 4727094..dd4ff14 100644
--- a/docz/data/engines.xls
+++ b/docz/data/engines.xls
@@ -244,7 +244,7 @@
   </WorksheetOptions>
  </Worksheet>
  <Worksheet ss:Name="Bindings">
-  <Table ss:ExpandedColumnCount="8" ss:ExpandedRowCount="16" x:FullColumns="1" x:FullRows="1" ss:DefaultColumnWidth="65" ss:DefaultRowHeight="16">
+  <Table ss:ExpandedColumnCount="8" ss:ExpandedRowCount="17" x:FullColumns="1" x:FullRows="1" ss:DefaultColumnWidth="65" ss:DefaultRowHeight="16">
    <Column ss:Index="3" ss:Width="24"/>
    <Column ss:Width="31"/>
    <Column ss:Width="24"/>
@@ -357,6 +357,16 @@
     <Cell ss:StyleID="s16"><Data ss:Type="String">✔</Data></Cell>
     <Cell ss:StyleID="s16"><Data ss:Type="String">✔</Data></Cell>
    </Row>
+   <Row>
+    <Cell ss:StyleID="s20" ss:HRef="/docs/demos/engines/jsc#rust"><Data ss:Type="String">JSC</Data></Cell>
+    <Cell><Data ss:Type="String">Rust</Data></Cell>
+    <Cell ss:StyleID="s16"/>
+    <Cell ss:StyleID="s16"><Data ss:Type="String">✔</Data></Cell>
+    <Cell ss:StyleID="s16"/>
+    <Cell ss:StyleID="s16"/>
+    <Cell ss:StyleID="s16"/>
+    <Cell ss:StyleID="s16"/>
+   </Row>
    <Row>
     <Cell ss:StyleID="s20" ss:HRef="/docs/demos/engines/rb#complete-example"><Data ss:Type="String">ExecJS</Data></Cell>
     <Cell><Data ss:Type="String">Ruby</Data></Cell>
diff --git a/docz/docs/02-getting-started/02-examples/06-loader.md b/docz/docs/02-getting-started/02-examples/06-loader.md
index 25ae310..ec8a955 100644
--- a/docz/docs/02-getting-started/02-examples/06-loader.md
+++ b/docz/docs/02-getting-started/02-examples/06-loader.md
@@ -49,35 +49,31 @@ SheetJS users have verified this demo in other configurations:
 <details>
   <summary><b>Other tested configurations</b> (click to show)</summary>
 
-| Platform                                                          | Architecture | Demo        |
-|:------------------------------------------------------------------|:-------------|:------------|
-| NVIDIA L40 (48 GB VRAM) + i9-13900K (32 GB RAM)                   | `linux-x64`  | LangChainJS |
-| NVIDIA RTX 4080 SUPER (16 GB VRAM) + Ryzen Z1 Extreme (16 GB RAM) | `win11-x64`  | LangChainJS |
-| NVIDIA RTX 4070 Ti (12 GB VRAM) + Ryzen 7 5800x (64 GB RAM)       | `win11-x64`  | LangChainJS |
-| NVIDIA RTX 4060 (8 GB VRAM) + Ryzen 7 5700g (32 GB RAM)           | `win11-x64`  | LangChainJS |
-| NVIDIA RTX 3090 (24 GB VRAM) + Ryzen 9 3900XT (128 GB RAM)        | `win11-x64`  | LangChainJS |
-| NVIDIA RTX 3080 (12 GB VRAM) + Ryzen 7 5800X (32 GB RAM)          | `win11-x64`  | LangChainJS |
-| NVIDIA RTX 3070 (8 GB VRAM) + Ryzen Z1 Extreme (16 GB RAM)        | `win11-x64`  | LangChainJS |
-| NVIDIA RTX 3060 (12 GB VRAM) + i5-11400 (32 GB RAM)               | `win10-x64`  | LangChainJS |
-| NVIDIA RTX 2080 (12 GB VRAM) + i7-9700K (16 GB RAM)               | `win10-x64`  | LangChainJS |
-| NVIDIA RTX 2070 (8 GB VRAM) + Ryzen 7 3700x (80 GB RAM)           | `linux-x64`  | LangChainJS |
-| NVIDIA RTX 2060 (6 GB VRAM) + Ryzen 5 3600 (32 GB RAM)            | `win10-x64`  | LangChainJS |
-| NVIDIA GTX 1080 (8 GB VRAM) + Ryzen 7 5800x (64 GB RAM)           | `win10-x64`  | LangChainJS |
-| NVIDIA GTX 1070 (8 GB VRAM) + Ryzen 7 7700x (32 GB RAM)           | `win11-x64`  | LangChainJS |
-| AMD RX 6800 XT (16 GB VRAM) + Ryzen Z1 Extreme (16 GB RAM)        | `win11-x64`  | LangChainJS |
-| Apple M4 10-Core CPU + 10-Core GPU (24 GB unified memory)         | `darwin-arm` | LangChainJS |
+| Platform                                                             | Architecture | Demo        |
+|:---------------------------------------------------------------------|:-------------|:------------|
+| NVIDIA L40 (48 GB VRAM) + i9-13900K (32 GB RAM)                      | `linux-x64`  | LangChainJS |
+| NVIDIA RTX 4080 SUPER (16 GB VRAM) + Ryzen Z1 Extreme (16 GB RAM)    | `win11-x64`  | LangChainJS |
+| NVIDIA RTX 4070 Ti SUPER (16 GB VRAM) + Ryzen Z1 Extreme (16 GB RAM) | `win11-x64`  | LangChainJS |
+| NVIDIA RTX 4070 Ti (12 GB VRAM) + Ryzen 7 5800x (64 GB RAM)          | `win11-x64`  | LangChainJS |
+| NVIDIA RTX 4060 (8 GB VRAM) + Ryzen 7 5700g (32 GB RAM)              | `win11-x64`  | LangChainJS |
+| NVIDIA RTX 3090 (24 GB VRAM) + Ryzen 9 3900XT (128 GB RAM)           | `win11-x64`  | LangChainJS |
+| NVIDIA RTX 3080 (12 GB VRAM) + Ryzen 7 5800X (32 GB RAM)             | `win11-x64`  | LangChainJS |
+| NVIDIA RTX 3070 (8 GB VRAM) + Ryzen Z1 Extreme (16 GB RAM)           | `win11-x64`  | LangChainJS |
+| NVIDIA RTX 3060 (12 GB VRAM) + i5-11400 (32 GB RAM)                  | `win10-x64`  | LangChainJS |
+| NVIDIA RTX 2080 (12 GB VRAM) + i7-9700K (16 GB RAM)                  | `win10-x64`  | LangChainJS |
+| NVIDIA RTX 2070 (8 GB VRAM) + Ryzen 7 3700x (80 GB RAM)              | `linux-x64`  | LangChainJS |
+| NVIDIA RTX 2060 (6 GB VRAM) + Ryzen 5 3600 (32 GB RAM)               | `win10-x64`  | LangChainJS |
+| NVIDIA GTX 1080 (8 GB VRAM) + Ryzen 7 5800x (64 GB RAM)              | `win10-x64`  | LangChainJS |
+| NVIDIA GTX 1070 (8 GB VRAM) + Ryzen 7 7700x (32 GB RAM)              | `win11-x64`  | LangChainJS |
+| AMD RX 6800 XT (16 GB VRAM) + Ryzen Z1 Extreme (16 GB RAM)           | `win11-x64`  | LangChainJS |
+| Apple M4 10-Core CPU + 10-Core GPU (24 GB unified memory)            | `darwin-arm` | LangChainJS |
 
 </details>
 
-Special thanks to:
+Special thanks to the following users for testing with multiple configurations:
 
 - [Asadbek Karimov](https://asadk.dev/)
-- [Nathan Ayers](https://infocommww.org/)
 - [Rasmus Tengstedt](https://tengstedt.dev/)
-- [Triston Armstrong](https://tristonarmstrong.com/)
-- [Navid Nami](https://navidnami.com/)
-- [Andreas Karydopoulos](https://smor.dev/)
-- [Tim Brugman](https://timbrugman.com/)
 
 :::
 
diff --git a/docz/docs/03-demos/01-math/01-summary.md b/docz/docs/03-demos/01-math/01-summary.md
index a6d302c..96e49bb 100644
--- a/docz/docs/03-demos/01-math/01-summary.md
+++ b/docz/docs/03-demos/01-math/01-summary.md
@@ -514,10 +514,11 @@ The van Reeken array mean can be implemented in one line of JavaScript code:
 for(var n = 1, mean = 0; n <= x.length; ++n) mean += (x[n-1] - mean)/n;
 ```
 
-<details>
-  <summary><b>Math details</b> (click to show)</summary>
+<details open>
+  <summary><b>Math details</b> (click to hide)</summary>
 
-Let $M[x;m] = \frac{1}{m}\sum_{i=1}^{m}x_m$ be the mean of the first $m$ elements. Then:
+Let $M[x;m] = \frac{1}{m}\sum_{i=1}^{m}x_i$ be the mean of the first $m$ elements
+(where $m > 0$). The mean of the first $m+1$ elements follows a simple relation:
 
 <table style={bs}>
   <tbody style={bs}>
@@ -537,6 +538,14 @@ $= \frac{1}{m+1}\sum_{i=1}^{m+1} x_i$
   <td style={bs}>&nbsp;</td>
   <td style={bs}>
 
+$= \frac{1}{m+1}((\sum_{i=1}^{m} x_i) + x_{m+1})$
+
+  </td>
+</tr>
+<tr style={bs}>
+  <td style={bs}>&nbsp;</td>
+  <td style={bs}>
+
 $= \frac{1}{m+1}\sum_{i=1}^{m} x_i + \frac{x_{m+1}}{m+1}$
 
   </td>
@@ -574,39 +583,114 @@ $= M[x;m] + \frac{x_{m+1}}{m+1} - \frac{1}{m+1}M[x;m]$
   </td>
 </tr>
 <tr style={bs}>
-  <td style={bs}>&nbsp;</td>
+  <td style={bs}>
+
+$M[x;m+1]$
+
+  </td>
   <td style={bs}>
 
 $= M[x;m] + \frac{1}{m+1}(x_{m+1}-M[x;m])$
 
   </td>
 </tr>
-<tr style={bs}>
-  <td style={bs}>
-
-$new\_mean$
-
-  </td>
-  <td style={bs}>
-
-$= old\_mean + (x_{m+1}-old\_mean) / (m+1)$
-
-  </td>
-</tr>
 
   </tbody>
 </table>
 
-Switching to zero-based indexing, the relation matches the following expression:
+---
+
+The mean of the first element is $M[x;1] = \frac{1}{1}\sum_{i=1}^{1}x_i = x_1$.
+By defining $M[x;0]=0$, the recurrence relation also holds for $m = 0$:
+
+<table style={bs}>
+  <tbody style={bs}>
+<tr style={bs}>
+  <td style={bs}>
+
+$x_1 = 0 + (x_1 - 0)$
+
+  </td>
+  <td style={bs}>
+
+$= 0 + \frac{1}{1}(x_1 - 0)$
+
+  </td>
+</tr>
+<tr style={bs}>
+  <td style={bs}>
+
+$M[x;1]$
+
+  </td>
+  <td style={bs}>
+
+$= M[x;0] + \frac{1}{1}(x_{1} - M[x;0])$
+
+  </td>
+</tr>
+<tr style={bs}>
+  <td style={bs}>
+
+$M[x;m+1]$
+
+  </td>
+  <td style={bs}>
+
+$= M[x;m] + \frac{1}{m+1}(x_{m+1} - M[x;m])$
+
+  </td>
+</tr>
+
+  </tbody>
+</table>
+
+$m = 0, M[x;0] = 0$ will serve as the initial condition for the recurrence.
+
+---
+
+JavaScript is zero-indexed: $x_1$ is `x[0]` and generally $x_{m}$ is `x[m-1]`.
+
+Adjusting for zero-based indexing, the following code calculates the recurrence:
 
 ```js
-new_mean = old_mean + (x[m] - old_mean) / (m + 1);
+/* data array */
+var x = [ /* ... data ... */ ];
+
+/* initial condition M[x;0] = 0 */
+var mean = 0;
+
+/* loop through each value in the array */
+for(var m = 0; m < x.length; ++m) {
+  /* store the old mean M[x;m] */
+  var mean_m  = mean;
+  /* get the next data point x_{m+1} */
+  var x_m1    = x[m];
+  /* calculate the new mean M[x;m+1] */
+  var mean_m1 = mean_m + (x_m1 - mean_m) / (m + 1);
+  /* update the mean */
+  mean = mean_m1;
+}
 ```
 
-This update can be succinctly implemented in JavaScript:
+This can be succinctly implemented by assigning to `mean` directly:
 
 ```js
-mean += (x[m] - mean) / (m + 1);
+for(var m = 0, mean = 0; m < x.length; ++m) {
+  mean = mean + (x[m] - mean) / (m + 1);
+}
+```
+
+This can be condensed further by using the addition assignment (`+=`) operator:
+
+```js
+for(var m = 0, mean = 0; m < x.length; ++m) mean += (x[m] - mean) / (m + 1);
+```
+
+Replacing `n = m+1` yields the final code expression:
+
+```js
+for(var n = 1, mean = 0; n <= x.length; ++n) mean += (x[n-1] - mean) / n;
 ```
 
 </details>
diff --git a/docz/docs/03-demos/17-mobile/07-lynx.md b/docz/docs/03-demos/17-mobile/07-lynx.md
index 2c6feb2..889d9f7 100644
--- a/docz/docs/03-demos/17-mobile/07-lynx.md
+++ b/docz/docs/03-demos/17-mobile/07-lynx.md
@@ -257,7 +257,7 @@ curl -o ./src/App.tsx https://docs.sheetjs.com/lynx/App.tsx
 ```bash
 curl -o ./src/App.css https://docs.sheetjs.com/lynx/App.css
 ```
-<a id="step5"></a>
+<a name="step5"></a>
 
 5) Start the development server:
 
diff --git a/docz/docs/03-demos/42-engines/04-jsc.md b/docz/docs/03-demos/42-engines/04-jsc.md
index de4003a..07cd0b2 100644
--- a/docz/docs/03-demos/42-engines/04-jsc.md
+++ b/docz/docs/03-demos/42-engines/04-jsc.md
@@ -748,6 +748,155 @@ swiftc -Xcc -I$(pwd) -Xlinker -L../WebKit-WebKit-7620.2.4.111.7/WebKitBuild/JSCO
 If successful, a CSV will be printed to console. The program also tries to write
 to `SheetJSwift.xlsx`, which can be opened in a spreadsheet editor.
 
+## Bindings
+
+It is straightforward to load the platform-native (macOS) or compiled libraries
+in programs built in other programming languages.
+
+### Rust
+
+Writing bindings is fairly mechanical. For example, the following C code
+creates a string within the engine from a UTF8 string:
+
+```c
+JSStringRef script = JSStringCreateWithUTF8CString("SheetJS");
+```
+
+**Rust Types**
+
+`JSStringRef` is a pointer to an opaque data type. The spiritual equivalent
+according to the Rustonomicon is a pointer to an opaque struct[^9]:
+
+```rust
+#[repr(C)]
+pub struct JSString {
+  _data: [u8; 0],
+  _marker: core::marker::PhantomData<(*mut u8, core::marker::PhantomPinned)>,
+}
+// highlight-next-line
+type JSStringRef = *mut JSContext;
+```
+
+**Function Declaration**
+
+The `JSStringCreateWithUTF8CString` function in declared in C as follows:
+
+```c
+JSStringRef JSStringCreateWithUTF8CString(const char * string);
+```
+
+The equivalent Rust declaration must be defined in an `extern "C"` block:
+
+```rust
+unsafe extern "C" {
+  pub unsafe fn JSStringCreateWithUTF8CString(string: *const u8) -> JSStringRef;
+}
+```
+
+**Rust Interchange**
+
+The `std::ffi` module includes a number of helpers for passing data between Rust
+code and C libraries.
+
+Rust string literals are commonly represented as `&str` types:
+
+```rust
+let script: &str = "'Sheet' + 'JS'"; // 'Sheet' + 'JS'
+```
+
+The following unsafe waltz creates a compatible pointer:
+
+A) Convert the `&str` to a byte slice
+
+B) Create a `std::ffi::CString` instance from the bytes
+
+C) Use the `as_ptr` method to generate a pointer
+
+```rust title="Generate a JSString from a Rust string literal"
+/* start with a string literal */
+let script: &str = "'Sheet' + 'JS'";
+
+/* generate CString */
+let cstr: std::ffi::CString = std::ffi::CString::new(script.as_bytes()).unwrap();
+
+/* call JSStringCreateWithUTF8CString */
+let ref: JSStringRef = JSStringCreateWithUTF8CString(cstr.as_ptr() as *const u8);
+```
+
+The demo makes a safe wrapper to perform the unsafe waltz in one line:
+
+```rust
+pub struct JSC;
+impl JSC {
+  pub fn JSStringCreateWithUTF8CString(str: &str) -> JSStringRef { unsafe {
+    // highlight-next-line
+    JSStringCreateWithUTF8CString(std::ffi::CString::new(str.as_bytes()).unwrap().as_ptr() as *const u8)
+  } }
+}
+```
+
+#### Rust Example
+
+:::note Tested Deployments
+
+This demo was last tested in the following deployments:
+
+| Architecture | Date       |
+|:-------------|:-----------|
+| `darwin-arm` | 2025-03-30 |
+
+:::
+
+1) Create a new project:
+
+```bash
+cargo new sheetjs-jsc
+cd sheetjs-jsc
+cargo run
+```
+
+2) Download the SheetJS Standalone script to the `src` folder in the project:
+
+<ul>
+<li><a href={`https://cdn.sheetjs.com/xlsx-${current}/package/dist/xlsx.full.min.js`}>xlsx.full.min.js</a></li>
+</ul>
+
+<CodeBlock language="bash">{`\
+curl -L -o src/xlsx.full.min.js https://cdn.sheetjs.com/xlsx-${current}/package/dist/xlsx.full.min.js`}
+</CodeBlock>
+
+3) Download the test file to the project directory:
+
+<ul>
+<li><a href="https://docs.sheetjs.com/pres.numbers">pres.numbers</a></li>
+</ul>
+
+<CodeBlock language="bash">{`\
+curl -LO https://docs.sheetjs.com/pres.numbers`}
+</CodeBlock>
+
+4) Download [`main.rs`](pathname:///jsc/main.rs) and replace `src/main.rs`:
+
+```bash
+curl -L -o src/main.rs https://docs.sheetjs.com/jsc/main.rs
+```
+
+5) Download [`build.rs`](pathname:///jsc/build.rs) to the project folder:
+
+```bash
+curl -LO https://docs.sheetjs.com/jsc/build.rs
+```
+
+6) Build and run the app:
+
+```bash
+cargo run pres.numbers
+```
+
+If the program succeeded, the CSV contents will be printed to console and the
+file `sheetjsw.xlsb` will be created. That file can be opened with a spreadsheet
+editor that supports XLSB spreadsheets.
+
 [^1]: See [`read` in "Reading Files"](/docs/api/parse-options)
 [^2]: See [`writeFile` in "Writing Files"](/docs/api/write-options)
 [^3]: See [`JSObjectMakeTypedArrayWithBytesNoCopy`](https://developer.apple.com/documentation/javascriptcore/jsobjectmaketypedarraywithbytesnocopy(_:_:_:_:_:_:_:)/) in the JavaScriptCore documentation.
@@ -757,3 +906,4 @@ https://developer.apple.com/documentation/javascriptcore/jsobjectgettypedarrayby
 [^6]: See [`read` in "Reading Files"](/docs/api/parse-options)
 [^7]: See [`writeFile` in "Writing Files"](/docs/api/write-options)
 [^8]: See ["Install Swift"](https://www.swift.org/install) in the Swift website.
+[^9]: See ["Representing opaque structs"](https://doc.rust-lang.org/nomicon/ffi.html#representing-opaque-structs) in the Rustonomicon
\ No newline at end of file
diff --git a/docz/static/jsc/build.rs b/docz/static/jsc/build.rs
new file mode 100644
index 0000000..74a61ed
--- /dev/null
+++ b/docz/static/jsc/build.rs
@@ -0,0 +1,4 @@
+#[cfg(target_os = "macos")]
+fn main() {
+    println!("cargo::rustc-link-lib=framework=JavaScriptCore");
+}
diff --git a/docz/static/jsc/main.rs b/docz/static/jsc/main.rs
new file mode 100644
index 0000000..5f10ec6
--- /dev/null
+++ b/docz/static/jsc/main.rs
@@ -0,0 +1,228 @@
+/* See https://doc.rust-lang.org/nomicon/ffi.html#representing-opaque-structs */
+#![allow(non_camel_case_types, non_snake_case, non_upper_case_globals)]
+
+#[repr(C)]
+pub struct JSContext {
+  _data: [u8; 0],
+  _marker: core::marker::PhantomData<(*mut u8, core::marker::PhantomPinned)>,
+}
+type JSContextRef = *mut JSContext;
+type JSGlobalContextRef = JSContextRef;
+
+#[repr(C)]
+pub struct JSClass {
+  _data: [u8; 0],
+  _marker: core::marker::PhantomData<(*mut u8, core::marker::PhantomPinned)>,
+}
+type JSClassRef = *mut JSClass;
+
+#[repr(C)]
+pub struct JSValue {
+  _data: [u8; 0],
+  _marker: core::marker::PhantomData<(*mut u8, core::marker::PhantomPinned)>,
+}
+type JSValueRef = *mut JSValue;
+/* NOTE: JSEvaluateScript takes a pointer-to-a-pointer, hence RefRef */
+type JSValueRefRef = *mut JSValueRef;
+
+#[repr(C)]
+pub struct JSObject {
+  _data: [u8; 0],
+  _marker: core::marker::PhantomData<(*mut u8, core::marker::PhantomPinned)>,
+}
+type JSObjectRef = *mut JSObject;
+
+#[repr(C)]
+pub struct JSString {
+  _data: [u8; 0],
+  _marker: core::marker::PhantomData<(*mut u8, core::marker::PhantomPinned)>,
+}
+type JSStringRef = *mut JSContext;
+
+#[repr(C)]
+pub struct BytesDeallocator {
+  _data: [u8; 0],
+  _marker: core::marker::PhantomData<(*mut u8, core::marker::PhantomPinned)>,
+}
+type JSTypedArrayBytesDeallocator = *mut BytesDeallocator;
+
+#[repr(C)]
+pub struct BytesDeallocatorContext {
+  _data: [u8; 0],
+  _marker: core::marker::PhantomData<(*mut u8, core::marker::PhantomPinned)>,
+}
+type JSTypedArrayBytesDeallocatorContext =  *mut BytesDeallocatorContext;
+
+/* NOTE: this is technically an enum */
+type JSTypedArrayType = u32;
+const kJSTypedArrayTypeUint8Array: u32 = 3;
+
+/* NOTE: this is technically "unsigned int" */
+type JSPropertyAttributes = u32;
+
+unsafe extern "C" {
+  pub unsafe fn JSEvaluateScript(ctx: JSContextRef, script: JSStringRef, thisObject: JSObjectRef, sourceURL: JSStringRef, startingLineNumber: i32, exception: JSValueRefRef) -> JSValueRef;  
+
+  pub unsafe fn JSGlobalContextCreate(string: JSClassRef) -> JSGlobalContextRef;
+  pub unsafe fn JSGlobalContextRelease(ctx: JSGlobalContextRef);
+
+  pub unsafe fn JSStringCreateWithUTF8CString(string: *const u8) -> JSStringRef;
+  pub unsafe fn JSStringGetUTF8CString(string: JSStringRef, buffer: *const u8, bufferSize: usize) -> usize;
+  pub unsafe fn JSStringGetMaximumUTF8CStringSize(string: JSStringRef) -> usize;
+  pub unsafe fn JSStringRelease(string: JSStringRef);
+
+  pub unsafe fn JSValueIsString(ctx: JSContextRef, value: JSValueRef) -> bool;
+  pub unsafe fn JSValueToStringCopy(ctx: JSContextRef, value: JSValueRef, exception: JSValueRefRef) -> JSStringRef;
+  pub unsafe fn JSValueToObject(ctx: JSContextRef, value: JSValueRef, exception: JSValueRefRef) -> JSObjectRef;
+
+  pub unsafe fn JSObjectGetTypedArrayLength(ctx: JSContextRef, object: JSObjectRef, exception: JSValueRefRef) -> usize;
+  pub unsafe fn JSObjectGetTypedArrayBytesPtr(ctx: JSContextRef, object: JSObjectRef, exception: JSValueRefRef) -> *const u8;
+  pub unsafe fn JSObjectSetProperty(ctx: JSContextRef, object: JSObjectRef, propertyName: JSStringRef, value: JSValueRef, attributes: JSPropertyAttributes, exception: JSValueRefRef);
+  pub unsafe fn JSObjectMakeTypedArrayWithBytesNoCopy(ctx: JSContextRef, arrayType: JSTypedArrayType, bytes: *const u8, byteLength: usize, bytesDeallocator: JSTypedArrayBytesDeallocator, deallocatorContext: JSTypedArrayBytesDeallocatorContext, exception: JSValueRefRef) -> JSObjectRef;
+
+  pub unsafe fn JSContextGetGlobalObject(ctx: JSContextRef) -> JSObjectRef;
+}
+
+macro_rules! NULL { () => { std::ptr::null_mut() }; }
+
+pub struct JSC;
+impl JSC {
+  pub fn JSEval(ctx: JSContextRef, script: &str) -> JSValueRef { unsafe {
+    let script: JSStringRef = JSStringCreateWithUTF8CString(std::ffi::CString::new(script.as_bytes()).unwrap().as_ptr() as *const u8);
+    let result: JSValueRef = JSEvaluateScript(ctx, script, NULL!(), NULL!(), 0, NULL!()); 
+    JSStringRelease(script);
+    result
+  } }
+  pub fn JSEvaluateScript(ctx: JSContextRef, script: JSStringRef, thisObject: JSObjectRef, sourceURL: JSStringRef, startingLineNumber: i32, exception: JSValueRefRef) -> JSValueRef { unsafe {
+    JSEvaluateScript(ctx, script, thisObject, sourceURL, startingLineNumber, exception)
+  } }
+
+  pub fn JSGlobalContextCreate(globalObjectClass: JSClassRef) -> JSGlobalContextRef { unsafe {
+    JSGlobalContextCreate(globalObjectClass)
+  } }
+  pub fn JSGlobalContextRelease(globalObjectClass: JSGlobalContextRef) { unsafe {
+    JSGlobalContextRelease(globalObjectClass)
+  } }
+
+  pub fn JSStringCreateWithUTF8CString(str: &str) -> JSStringRef { unsafe {
+    JSStringCreateWithUTF8CString(std::ffi::CString::new(str.as_bytes()).unwrap().as_ptr() as *const u8)
+  } }
+  pub fn JSStringRelease(string: JSStringRef) { unsafe {
+    JSStringRelease(string)
+  } }
+  pub fn JSCastStr(ctx: JSContextRef, value: JSValueRef) -> Result<String, String> { unsafe {
+    match JSValueIsString(ctx, value) {
+      true => {
+        /* TODO: this leaks memory -- `buf` must be freed and `str` must be JSStringRelease-d */
+        let str: JSStringRef = JSValueToStringCopy(ctx, value, NULL!());
+        let sz: usize = JSStringGetMaximumUTF8CStringSize(str);
+        let layout = std::alloc::Layout::array::<u8>(sz).unwrap();
+        let buf = std::alloc::alloc(layout) as *mut u8;
+        match buf.is_null() {
+          true => Err(format!("Unable to allocate {} bytes", sz)),
+          false => {
+            let size = JSStringGetUTF8CString(str, buf, sz);
+            let slice = std::slice::from_raw_parts(buf, size);
+            String::from_utf8(slice.to_vec()).map_err(|e| format!("from_utf8 error {}", e))
+          }
+        }
+      },
+      false => Err("Value is not a string".to_string())
+    }
+  } }
+
+  pub fn JSObjectSetProperty(ctx: JSContextRef, object: JSObjectRef, propertyName: JSStringRef, value: JSValueRef, attributes: JSPropertyAttributes, exception: JSValueRefRef) { unsafe { 
+    JSObjectSetProperty(ctx, object, propertyName, value, attributes, exception)
+  } }
+  pub fn JSObjectMakeUint8Array(ctx: JSContextRef, data: Vec<u8>) -> JSObjectRef { unsafe {
+    JSObjectMakeTypedArrayWithBytesNoCopy(ctx, kJSTypedArrayTypeUint8Array, data.as_ptr(), data.len(), NULL!(), NULL!(), NULL!())
+  } }
+
+  pub fn JSContextGetGlobalObject(ctx: JSContextRef) -> JSObjectRef { unsafe {
+    JSContextGetGlobalObject(ctx)
+  } }
+  pub fn JSContextSetGlobalProperty(ctx: JSContextRef, key: &str, val: JSValueRef) {
+    let global: JSObjectRef = JSC::JSContextGetGlobalObject(ctx);
+    let k: JSStringRef = JSC::JSStringCreateWithUTF8CString(key);
+    JSC::JSObjectSetProperty(ctx, global, k, val, 0, NULL!());
+    JSC::JSStringRelease(k);
+
+  }
+
+  pub fn JSCastObjectToValue(obj: JSObjectRef) -> JSValueRef { obj as JSValueRef }
+  pub fn JSEvalAB(ctx: JSContextRef, script: &str) -> Vec<u8> { unsafe {
+    let result = JSC::JSEval(ctx, script);
+
+    /* pull Uint8Array data back to Rust */
+    let u8: JSObjectRef = JSValueToObject(ctx, result, NULL!());
+    let sz: usize = JSObjectGetTypedArrayLength(ctx, u8, NULL!());
+    let buf: *const u8 = JSObjectGetTypedArrayBytesPtr(ctx, u8, NULL!());
+
+    std::slice::from_raw_parts(buf, sz).to_vec()
+  } }
+  pub fn JSEvalStr(ctx: JSContextRef, script: &str) -> Result<String, String> {
+    let result = JSC::JSEval(ctx, script);
+    JSC::JSCastStr(ctx, result)
+  }
+}
+
+
+fn main() {
+
+  /* initialize */
+  let ctx: JSGlobalContextRef = JSC::JSGlobalContextCreate(NULL!());
+
+  /* JSC does not expose a standard "global" by default */
+  JSC::JSEval(ctx, "var global = (function(){ return this; }).call(null);"); 
+
+  /* load library */
+  JSC::JSEval(ctx, include_str!("xlsx.full.min.js")); 
+
+  /* get version string */
+  match JSC::JSEvalStr(ctx, "XLSX.version") {
+    Ok(s) => { println!("SheetJS library version {}", s); },
+    Err(e) => {
+      eprintln!("Could not get SheetJS version: {}", e);
+      JSC::JSGlobalContextRelease(ctx);
+      std::process::exit(1)
+    }
+  }
+
+  /* read file */
+  {
+    let mut iter = std::env::args();
+    let path: String = iter.nth(1).expect("must specify a file name");
+
+    let file: Vec<u8> = std::fs::read(path.clone()).unwrap();
+  
+    /* push data to JSC */
+    let u8: JSObjectRef = JSC::JSObjectMakeUint8Array(ctx, file);
+
+    /* assign to `global.buf` */
+    JSC::JSContextSetGlobalProperty(ctx, "buf", JSC::JSCastObjectToValue(u8));
+  }
+
+  /* parse workbook and print CSV */
+  match JSC::JSEvalStr(ctx, "
+    var wb = XLSX.read(global.buf);
+    var ws = wb.Sheets[wb.SheetNames[0]];
+    XLSX.utils.sheet_to_csv(ws)
+  ") {
+    Ok(s) => { println!("{}", s); },
+    Err(e) => {
+      eprintln!("Could not generate CSV: {}", e);
+      JSC::JSGlobalContextRelease(ctx);
+      std::process::exit(2)
+    }
+  }
+
+  /* write file */
+  {
+    let result = JSC::JSEvalAB(ctx, "XLSX.write(wb, {type:'buffer', bookType:'xlsb'});");
+
+    /* save file */
+    std::fs::write("sheetjsw.xlsb", result).unwrap();
+  }
+
+  JSC::JSGlobalContextRelease(ctx);
+}