Is it possible to send a closure to a Rust WASM plugin?

I want to create a Rust plugin which can solve non-linear equations.
Therefore, I’d like to specify the equation as a closure in Typst and use it as a function pointer in Rust.

Something like

let f(x) = calc.sin(x) - x
plugin.solve_root(f)

and

#[wasm_func]
pub fn solve_root(f: &[u8]) -> Vec<u8> {
    let f: Box<dyn Fn(f64) -> f64> = ciborium::from_reader(f).unwrap();
    ///...
}

However, the Rust code fails with

error[E0277]: the trait bound `dyn Fn(f64) -> f64: serde::de::Deserialize<'_>` is not satisfied

Is there any way to send a closure from Typst to a Rust WASM plugin?

Do you have a source code for this? Without deep knowledge, it’s hard to debug because you need to first write the rest (cuz Rust), and you are not showing the rest.

For other readers, I’ll mention that this is a crosspost from Discord, where the post got the following response:

(there were other responses, but this is the most relevant)

Calling back into Typst: the problem of coroutines

You will indeed need to implement coroutines. Basically the flow would be like this:

  1. your Typst code calls the WASM plugin. Instead of passing a closure, the Typst code would instead need to pass a “handle” (e.g. an integer) to the plugin

  2. the plugin executes, then realizes it needs to call the Typst function. Instead of doing that, it would need to return to Typst and signal “I’m not done, but I need to call function handle with the following parameters: *data*”. In Typst, you would then call the function, and return the result to the plugin by again calling a function of the plugin.

    Now you need to handle an additional problem: plugins are pure, that means they can’t keep any state between invocations. That means you need to do one of two things:

    • use the transition API to allow the plugin call to remember that the initial call, before requesting your Typst function, had an effect; or
    • return not only the parameters from your plugin, but also any transient state that the plugin has produced, and manually recreate that state when you call the plugin again
  3. at some point, the plugin will not require any more Typst function calls and return a final result.

Step 2 is obviously the hard one. I would probably prefer the first variant I outlined, but that is also not trivial and I’m not sure what the performance impacts would be.

Avoiding coroutines: use an AST!

I would recommend to either implement this in Typst, or replace the function with something that can be transferred to Rust. For example, maybe you can replace the function

x => calc.sin(x) - x

with the dictionary

(
  parameters: ("x",),
  expression: (
    kind: "binary",
    operator: "-",
    left: (
      kind: "function",
      name: "sin",
      parameters: (
        (kind: "parameter", name: "x"),
      ),
    ),
    right: (kind: "parameter", name: "x"),
  ),
)

This is basically an abstract syntax tree (AST) of that function. Instead of creating this completely manually, you could of course start with math content such as $x => sin(x) - x$ and use techniques similar to those employed by eqalc – Typst Universe (not 100% sure that’s sufficient, haven’t looked at the code in detail), and go from there on the Rust side.

5 Likes

The rest of the source code doesn’t really matter. The problem is that ciborium can’t deserialize a function :)

I also asked on Discord (in the same thread) whether it’s possible to create an AST from Typst code inside Typst itself. The answer was there’s sadly no built-in way to do it.
I also had a look at eqalc, but I wanted to have an actual closure as the input type.

I’m not sure if this will help at all, but there is GitHub - freundTech/typst-matryoshka: Work in progress typst-in-typst plugin.