Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/knut

crates/knut-bifrost/wasm-guest/src/lib.rs

//! A **real** `wasm32-unknown-unknown` guest for the `WasmTransform` host in
//! `crates/knut-bifrost/src/wasm.rs`. Arrow IPC **stream** in, Arrow IPC stream
//! out — proving arrow-on-wasm works, and a template for users' own guests.
//!
//! ## Transform logic
//!
//! Read the input record batches, **keep only the rows where the `_gar_id`
//! column is even**, and write the surviving rows back as an Arrow IPC stream.
//! `_gar_id` is read as `Int64` (the column the host turns into vertex ids).
//!
//! ## Guest ABI (must match the host)
//!
//! - `memory` — linear memory (exported automatically for `cdylib`).
//! - `alloc(len: i32) -> i32` — reserve `len` bytes, return the offset; the host
//!   writes the input IPC stream there. Implemented as a `Vec`-leak.
//! - `transform(ptr: i32, len: i32) -> i64` — read the IPC at `[ptr, ptr+len)`,
//!   filter, write an IPC stream into a leaked buffer, and return it packed as
//!   `(out_ptr as i64) << 32 | (out_len as i64)`.
//!
//! Build: `cargo build --target wasm32-unknown-unknown --release`.

#![no_std]

extern crate alloc;

use alloc::vec::Vec;
use core::mem;

use arrow_array::cast::AsArray;
use arrow_array::types::Int64Type;
use arrow_array::{BooleanArray, RecordBatch};
use arrow_ipc::reader::StreamReader;
use arrow_ipc::writer::StreamWriter;
use arrow_select::filter::filter_record_batch;

/// Reserve `len` bytes in guest memory and return the offset. The host writes
/// the input Arrow-IPC stream at the returned pointer.
///
/// Implemented as a `Vec`-leak: allocate a `Vec<u8>` of the requested length,
/// hand out its pointer, and forget it so the allocation outlives the call.
/// `transform` reconstructs nothing from it — it only reads through the raw
/// pointer the host passes back — so the bytes simply stay live for the call's
/// duration (a fresh `wasmi::Store` per `apply` means leaks don't accumulate
/// across host invocations).
#[no_mangle]
pub extern "C" fn alloc(len: i32) -> i32 {
    let len = len.max(0) as usize;
    let mut buf: Vec<u8> = Vec::with_capacity(len);
    // SAFETY: capacity == len; the bytes are about to be overwritten by the host.
    unsafe { buf.set_len(len) };
    let ptr = buf.as_mut_ptr();
    mem::forget(buf);
    ptr as i32
}

/// Read the input IPC stream, keep only even-`_gar_id` rows, write the result as
/// an IPC stream into a leaked buffer, and return `(out_ptr << 32) | out_len`.
///
/// On any error (bad IPC, missing/wrong-typed `_gar_id`, write failure) the
/// guest writes an **empty but valid** IPC stream (schema only, no batches) when
/// it can, or returns `(0, 0)` if even that fails — the host reads an empty
/// stream as zero output batches rather than trapping.
#[no_mangle]
pub extern "C" fn transform(ptr: i32, len: i32) -> i64 {
    let input: &[u8] = unsafe { core::slice::from_raw_parts(ptr as *const u8, len.max(0) as usize) };
    let out = run(input).unwrap_or_default();
    let out_len = out.len() as i64;
    let out_ptr = out.as_ptr() as i64;
    mem::forget(out); // hand ownership to the host; it reads then drops the Store.
    (out_ptr << 32) | (out_len & 0xFFFF_FFFF)
}

/// The actual transform: IPC → filter even `_gar_id` → IPC. Returns `None` on
/// any failure so the caller can fall back to `(0, 0)`.
fn run(input: &[u8]) -> Option<Vec<u8>> {
    let reader = StreamReader::try_new(input, None).ok()?;
    let schema = reader.schema();

    let mut out: Vec<u8> = Vec::new();
    {
        let mut writer = StreamWriter::try_new(&mut out, &schema).ok()?;
        for batch in reader {
            let batch = batch.ok()?;
            let filtered = filter_even_gar_id(&batch)?;
            writer.write(&filtered).ok()?;
        }
        writer.finish().ok()?;
    }
    Some(out)
}

/// Build a boolean mask `_gar_id % 2 == 0` and filter the batch with it. Nulls
/// in `_gar_id` are treated as non-matching (dropped). Returns `None` if there
/// is no `Int64` `_gar_id` column.
fn filter_even_gar_id(batch: &RecordBatch) -> Option<RecordBatch> {
    let col = batch.column_by_name("_gar_id")?;
    let ids = col.as_primitive_opt::<Int64Type>()?;

    let mask: BooleanArray = ids
        .iter()
        .map(|v| Some(v.map(|x| x % 2 == 0).unwrap_or(false)))
        .collect();

    filter_record_batch(batch, &mask).ok()
}