Skip to content

Conversation

Copy link

Copilot AI commented Jan 5, 2026

Implements a Rust-based Pure Data external embedding deno_core (V8) for ~100ns message latency, enabling audio-rate JavaScript/TypeScript processing. Complements existing IPC-based pd-node (~100μs latency) for control-rate use cases.

Architecture

Pure Data C FFI → Rust → deno_core → V8 → User Script

  • src/lib.rs: PD external interface (node_embed_setup, lifecycle, message handlers)
  • src/runtime.rs: JsRuntime initialization, TypeScript transpilation (deno_ast), script execution
  • src/ops/: Fast ops for outlet, post, error using #[op2(fast)]
  • src/loader.rs: Custom ModuleLoader with TS transpilation
  • js/bootstrap.js: Sets up globalThis.pd API and console redirection
  • build.rs: Generates PD header bindings via bindgen (fallback to manual)

API

// examples/hello.ts
pd.on('bang', () => {
    pd.outlet(0, Math.random());
    pd.post('Hello from V8!');
});

pd.on('float', (value: number) => {
    pd.outlet(0, value * 2);
});

Trade-offs

pd-node (IPC) pd-node-embed
Latency ~100μs ~100ns
Size <1MB 62MB (V8 included)
npm Full ecosystem Not yet supported
Use case Control-rate, APIs Audio-rate, DSP

Build

cd pd-node-embed
cargo build --release
# → target/release/libnode_embed.so (Linux)
# → target/release/libnode_embed.dylib (macOS)

Install by copying to PD externals as node~.pd_linux / node~.pd_darwin.

Future Work

  • Audio buffer shared memory ops (zero-copy DSP)
  • Hot reload via file watcher
  • Elementary Audio integration
  • Signal rate (~) inlet/outlet support
Original prompt

Overview

Create a new pd-node-embed/ directory with a Rust-based Pure Data external that embeds deno_core (V8) for near-native JavaScript/TypeScript execution. This provides ~100ns latency (vs ~100μs with the current IPC approach), enabling audio-rate JS processing.

Background

The current pd-node uses fork/exec + IPC pipes to communicate with system Bun/Node.js. This works great for control-rate messages but has ~50-100μs latency per message, which is too slow for audio-rate processing.

By embedding deno_core (the core of Deno runtime), we get:

  • ~1000x faster message passing via "Ops" (~10-100ns)
  • Shared memory for audio buffers (zero-copy DSP)
  • TypeScript transpilation built-in
  • Single process - no fork, no pipes, no context switches

Implementation Requirements

1. Project Structure

pd-node-embed/
├── Cargo.toml              # Rust project with deno_core dependency
├── build.rs                # Build script for Pd headers
├── src/
│   ├── lib.rs              # Pd external entry point (extern "C" functions)
│   ├── runtime.rs          # deno_core JsRuntime setup and management
│   ├── ops/
│   │   ├── mod.rs          # Op module exports
│   │   ├── outlet.rs       # op_outlet(outlet_idx, value) - send to Pd outlet
│   │   ├── post.rs         # op_post(message) - print to Pd console
│   │   └── inlet.rs        # op_get_inlet() - get current inlet number
│   └── loader.rs           # Custom ModuleLoader for .ts/.js files
├── js/
│   ├── bootstrap.js        # Runtime bootstrap (sets up globalThis.pd)
│   └── pd-api.ts           # TypeScript declarations for the pd API
├── examples/
│   ├── hello.ts            # Simple bang → outlet example
│   └── multiply.ts         # Float processing example
└── README.md               # Documentation for the embedded approach

2. Cargo.toml

[package]
name = "pd-node-embed"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]
name = "node_embed"

[dependencies]
deno_core = "0.280"         # Or latest compatible version
deno_ast = "0.38"           # For TypeScript transpilation

[build-dependencies]
bindgen = "0.69"            # Generate Rust bindings from m_pd.h

3. Core Rust Implementation (src/lib.rs)

Implement the Pure Data external interface:

use std::ffi::{c_void, CStr};
use std::os::raw::{c_char, c_int, c_float};

// Pd bindings (generated or manual)
mod pd_sys;
use pd_sys::*;

mod runtime;
mod ops;
mod loader;

use runtime::PdRuntime;

#[repr(C)]
pub struct t_node_embed {
    x_obj: t_object,
    runtime: Option<Box<PdRuntime>>,
    outlet: *mut t_outlet,
    script_path: String,
}

static mut NODE_EMBED_CLASS: *mut t_class = std::ptr::null_mut();

#[no_mangle]
pub unsafe extern "C" fn node_embed_setup() {
    NODE_EMBED_CLASS = class_new(
        gensym(b"node~\0".as_ptr() as *const c_char),
        Some(std::mem::transmute::<_, t_newmethod>(node_embed_new as *const ())),
        Some(std::mem::transmute::<_, t_method>(node_embed_free as *const ())),
        std::mem::size_of::<t_node_embed>(),
        0,
        A_GIMME,
        0,
    );
    
    class_addbang(NODE_EMBED_CLASS, Some(node_embed_bang));
    class_addfloat(NODE_EMBED_CLASS, Some(node_embed_float));
    
    post(b"[node~] pd-node-embed v0.1.0 - Embedded JS/TS runtime\0".as_ptr() as *const c_char);
}

unsafe extern "C" fn node_embed_bang(x: *mut t_node_embed) {
    if let Some(ref mut runtime) = (*x).runtime {
        runtime.call_handler("bang", &[]);
    }
}

unsafe extern "C" fn node_embed_float(x: *mut t_node_embed, f: c_float) {
    if let Some(ref mut runtime) = (*x).runtime {
        runtime.call_handler("float", &[f.into()]);
    }
}

4. deno_core Runtime (src/runtime.rs)

use deno_core::{JsRuntime, RuntimeOptions, op2, OpState};
use std::rc::Rc;
use std::cell::RefCell;

pub struct PdRuntime {
    js_runtime: JsRuntime,
    // Reference back to Pd object for ops
    pd_context: Rc<RefCell<PdContext>>,
}

impl PdRuntime {
    pub fn new(script_path: &str, outlet: *mut t_outlet) -> Result<Self, Error> {
        let pd_context = Rc::new(RefCell::new(PdContext { outlet }));
        
        let mut runtime = JsRuntime::new(RuntimeOptions {
            extensions: vec![pd_ops::init_ops(pd_context.clone())],
            module_loader: Some(Rc::new(loader::TsModuleLoader)),
            ..Default::default()
        });
        
        // Bootstrap the pd API
        runtime.execute_script("<bootstrap>", include_str!("../js/bootstrap.js"))?;
        
        // Load user script
        runtime.execute_script("<user>", &std::fs::read_to_string(script_path)?)?;
        
        Ok(Self { js_runtime: runtime, pd_context })
    }
    
    pub fn call_handler(&mut self, name: &str, args: &[JsValue]) {
        let script = format!("globalThis.__pd_dispatch__('{}', {:?})", name, args);
        let _ = self.js_runtime.execute_script("<dispatch>", &script);
    }
}

5. Ops Implementation...

This pull request was created from Copilot chat.


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI self-assigned this Jan 5, 2026
@coderabbitai
Copy link

coderabbitai bot commented Jan 5, 2026

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copilot AI and others added 2 commits January 5, 2026 18:51
Co-authored-by: theslyprofessor <20659937+theslyprofessor@users.noreply.github.com>
Co-authored-by: theslyprofessor <20659937+theslyprofessor@users.noreply.github.com>
Copilot AI changed the title [WIP] Create pd-node-embed directory for Rust-based Pure Data external Add pd-node-embed: Rust/deno_core external for sub-microsecond JS/TS execution Jan 5, 2026
Copilot AI requested a review from theslyprofessor January 5, 2026 19:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants