ChainSafe / ChainSafe/zapi

Feature: support returning a struct of class instances as a JS object

Open
#23 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Zig
Stars
4
Forks
4
Avg merge
16h 14m
Merged PRs (30d)
2

Description

## Summary

`js.convertReturn` currently handles two return shapes from DSL functions:

1. A single class type — auto-materialized into a JS instance.
2. A DSL-wrapper type (anything with `val: napi.Value`) — passed through directly.

There is no built-in path for returning a **JS object whose fields are themselves materialized class instances**. This forces consumers to hand-build the object with `createObject()` + per-field `js.convertReturn` + `setNamedProperty`, losing the DSL's auto-marshalling ergonomics.

## Motivation

Real-world example from BLS signature aggregation: the function needs to return `{ pk: PublicKey, sig: Signature }` where both are DSL-wrapped class types. The cleanest currently-possible code:

```zig
pub fn aggregateWithRandomness(sets: js.Array) !js.Value {
// ... compute result_pk: NativePublicKey, result_sig: NativeSignature ...
const env = js.env();
const result = try env.createObject();
try result.setNamedProperty("pk", .{
.env = env.env,
.value = js.convertReturn(PublicKey, .{ .raw = result_pk }, env.env),
});
try result.setNamedProperty("sig", .{
.env = env.env,
.value = js.convertReturn(Signature, .{ .raw = result_sig }, env.env),
});
return .{ .val = result };
}
```

The return type `!js.Value` carries no information about the JS shape; readers must consult the doc comment.

## Why none of today's shapes fit

- Returning a bare `struct { pk: PublicKey, sig: Signature }` hits `@compileError(\"convertReturn: unsupported return type ...\")` — it's neither a class nor a DSL wrapper.
- `!js.Object(struct { pk: PublicKey, sig: Signature })` compiles, but the inner struct is purely cosmetic — `Object(T).set` / `.get` would fail to instantiate because they assume each field has a `.val: napi.Value` accessor; class types expose `.raw` instead. Zig's lazy method-body resolution lets the annotation slip through unchecked, so it doesn't validate the actual JS object — misleading.
- `!js.Object(struct { pk: js.Value, sig: js.Value })` works with `.set(...)` but throws away the per-field type information and still needs manual `js.convertReturn` to wrap each class instance.

## Proposed feature

Extend `convertReturn` (or add a sibling primitive) so that a Zig struct whose fields are class types or DSL wrappers can be returned from a DSL function, and the runtime auto-builds a JS object with each field materialized appropriately.

```zig
const Result = struct { pk: PublicKey, sig: Signature };

pub fn aggregateWithRandomness(sets: js.Array) !Result {
// ...
return .{
.pk = .{ .raw = result_pk },
.sig = .{ .raw = result_sig },
};
}
```

Semantics: comptime iterate the struct fields, run `convertReturn` per field, assemble into a JS object via `napi_create_object` + `napi_set_named_property`. Mixed fields (class, DSL wrapper, raw `napi.Value`) should all work uniformly.

This would close the gap with typical N-API binding ergonomics (structured returns are common for crypto, serialization, multi-result helpers) and let the example return type be honest about its shape:

```zig
pub fn aggregateWithRandomness(sets: js.Array) !struct { pk: PublicKey, sig: Signature }
```

## Workaround today

A project-local helper closes the gap but arguably belongs in the DSL itself:

```zig
inline fn buildJsObject(comptime Shape: type, value: Shape) !js.Value {
const e = js.env();
const obj = try e.createObject();
inline for (@typeInfo(Shape).@\"struct\".fields) |field| {
const napi_val = napi.Value{
.env = e.env,
.value = js.convertReturn(field.type, @field(value, field.name), e.env),
};
try obj.setNamedProperty(field.name ++ \"\", napi_val);
}
return .{ .val = obj };
}
```

Encountered while migrating lodestar-z's blst NAPI bindings to the high-level DSL.

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.