Extern callback to async/await?

I want to use zig to write async wasm32-freestanding code.
I have already figured out how to pass a callback to an extern function, and receive the callback.

I’m having trouble figuring out how to make it use async/await/suspend/resume.
I think are gonna be multiple steps here but what I want is to call an exported function (from js, the wasm host) which will start an async process - it needs to create a frame, store it somewhere, call an extern function to register a callback, then return. later, calling that callback resumes the frame.

I’ve studied the example in learn zig Introduction | zig.guide but the examples seem to rely on the blocking of std.time.sleep()

I’ve tried a couple of things, and get weird errors.

What I don’t understand is where the @frame() is stored in memory. It seems a little like it’s sitting on the stack, so when I pass around pointers and try to call back it doesn’t exist anymore.

Okay I think I found it:

  const frame = allocator.create(@Frame(get)) catch unreachable;
  frame.* = async get(array[0..array.len]);

I couldn’t find any documentation that described this but I saw this issue
https://github.com/ziglang/zig/issues/1260#issuecomment-426430429

I don’t understand everything in that issue but I tried it and it worked!

my example code:

const allocator = @import("std").heap.page_allocator;

//onReady takes a callback and a frame pointer,
//and then at some point passes the frame pointer to the callback.
extern fn onReady(
  cb: fn ( frame: *@Frame(get) ) callconv(.C)  void,
  frame: *@Frame(get)
) void;

//copies memory from host into pointer, up to len
extern fn read(ptr: [*]const u8, len:usize) void;

fn get (slice: []u8) usize{
  //put a suspend here, otherwise async get()
  //will run to the end then return frame at return value.
  //we don't want to do that because memory to read isn't ready yet.
  suspend {}
  read(slice.ptr, slice.len);
  return slice.len;
}

fn cb (frame: *@Frame(get)) callconv(.C)  void { 
  defer allocator.destroy(frame);
  resume frame.*;
}

export fn init () void {
  var array:[11]u8 = [_]u8{0,0,0,0,0,0,0,0,0,0,0};
  const frame = allocator.create(@Frame(get)) catch unreachable;
  frame.* = async get(array[0..array.len]);
  onReady(cb, frame);
}

and then the javascript side:

const fs = require('fs');
const source = fs.readFileSync("./await2.wasm");
const typedArray = new Uint8Array(source);

;(async function () {
  var callback, frame
  var buffer = Buffer.alloc(0)
  var result = await WebAssembly.instantiate(typedArray, {
    env: {
    print: function (ptr, len) {
      var memory = Buffer.from(result.instance.exports.memory.buffer)
      console.log(memory.slice(ptr, ptr+len).toString())      
    },
    read: function (ptr, len) {
      var memory = Buffer.from(result.instance.exports.memory.buffer)
      buffer.copy(memory, ptr, 0, len)
      return len      
    },
    onReady: function (fn_ptr, frame) {
      var table = result.instance.exports.__indirect_function_table
      var memory = Buffer.from(result.instance.exports.memory.buffer)    
      var cb = table.get(fn_ptr)
      callback = () => cb(frame)
    },
  }})
  var r = ~~(Math.random()*1000)

  result.instance.exports.init(r)
  setTimeout(()=>{
    buffer = Buffer.from('hello world\n')
    callback()
  }, 1000)
}())

this is progress but I want to be able to use code that uses await, but is triggered by callbacks like this

I’m a bit puzzled here because what magic is happening in frame.* = async ... is it seeing that and creating that frame inside memory I allocated? it felt like it was normally this syntax would be used as to set the value of a pointer, making it a copy

watching this video, it says you can’t suspend a frame twice…

but if resumed the copy of a frame how will the compiler know that?

const allocator = @import("std").heap.page_allocator;
const print = @import("std").debug.print;

fn foo() void {
  suspend {}
  print("hello\n", .{});
}

pub fn main () !void {
   const frame1 = allocator.create(@Frame(foo)) catch unreachable;
   const frame2 = allocator.create(@Frame(foo)) catch unreachable;

  frame1.* = async foo();

  frame2.* = frame1.*;
  resume frame1; //1
  resume frame2; //2
}

whats the output does it compile? bets everyone!

hello
hello

yes it resumes twice!

so in conclusion… the value of async foo() is dropped off the stack when the main function returns, but since I cloned it, it can resume more than once.

but that must mean that the first frame is also valid?

   const frame1 = allocator.create(@Frame(foo)) catch unreachable;
   const frame2 = allocator.create(@Frame(foo)) catch unreachable;

  var frame = async foo(); //on the stack
  frame1.* = frame; //heap1
  frame2.* = frame; //heap2
  resume frame1; //1
  resume frame2; //2
  resume frame;  //3

and yes it outputs hello\n 3 times.

I haven’t done any async programming yet with Zig, but did you see the part on async in the Language Reference? It has quite a few examples.

I haven’t read that but I did read ziglearn. I have now read that as well but it didn’t cover anything like this, and not putting frames on the stack either

I think I found a clue:

fn foo_2 (frame: anyframe->void, msg: []const u8) void {
  await frame;
  print("{s}", .{msg});
}

...
  _ = async foo_2(&frame, "frame1"[0..]);
...
  resume frame;

now… it prints “frame1” after the frame resumes. I figured that await must some how register the await call site with the frame - i.e. so it has a pointer back from that frame. not sure how I’ll use this but I think it might be a key

digging deeper? does an awaited from point to the awaiter frame?

//print the raw bytes of a frame:
//(cast to raw bytes, then convert to slice)
pub fn print_frame (frame1: *@Frame(foo)) void {
  print("frame bytes:{s}\n", .{
    fmt.fmtSliceHexUpper(@ptrCast([*]u8, frame1)[0..(@sizeOf(@Frame(foo)))])
  });
}
  var frame = async foo();
  frame1.* = frame;
  print_frame(frame1);
  var foo2_frame = async foo_2(frame1, "frame1"[0..]);
  print_frame(frame1);
  print("ptr size:{d}\n", .{@sizeOf(*@Frame(foo))});
  print("foo_2 addr {X}\n", .{@ptrToInt(&foo2_frame)});
  resume frame1; //1
  print_frame(frame1);

output:

frame bytes:804723000000000002000000000000000000000000000000 //created frame
frame bytes:8047230000000000020000000000000000ED21C6FE7F0000 //after await
foo_2 addr 7FFEC621ED00
hello
6 6672616d6531frame size:24
frame bytes:8047230000000000FFFFFFFFFFFFFFFFFF12DE390180FFFF //after end
hello

each byte of 7FFE C621 ED00 is the reverse of 00ED 21C6 FE7F 0000 because it’s in little endian order.

so, CONFIRMED.

after some help from zig discord, I got have solved this: