Feature: support returning a struct of class instances as a JS object
Nobody has claimed this yet.
Assessment
- Difficulty
- 5/5
- Estimated time
- Over a week
- Newbie friendliness
- 35/100
Research direction
Start at js.convertReturn and inspect the existing class and DSL-wrapper handling, then compare it with the buildJsObject workaround and the createObject/setNamedProperty APIs shown here. Define how comptime struct fields, mixed field types, errors, and empty structs should behave, and add coverage for the proposed structured return semantics before considering the feature complete.
Written by the indexing model from the issue text.
Description
Summary
js.convertReturn currently handles two return shapes from DSL functions:
- A single class type — auto-materialized into a JS instance.
- 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:
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/.getwould fail to instantiate because they assume each field has a.val: napi.Valueaccessor; class types expose.rawinstead. 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 manualjs.convertReturnto 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.
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:
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:
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.
- Dominant language
- Zig
- Stars
- 4
- Forks
- 4
- PR merge metrics
- No merged PRs in 30d
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from ChainSafe/zapi
-
Difficulty 1/5 Under an hour Newbie friendliness 92/100
-
Type: Maintenance
Difficulty 1/5 Under an hour Newbie friendliness 76/100
-
clearer errors Open
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Similar issues
-
bug clawsweeper:linked-pr-open clawsweeper:needs-live-repro clawsweeper:no-new-fix-pr impact:message-loss issue-rating: 🐚 platinum hermit P2 regression
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
Difficulty 1/5 Under an hour Newbie friendliness 90/100
AXERA-TECH/ax-llm#77 ·
-
Difficulty 1/5 Under an hour Newbie friendliness 90/100
games-on-whales/wolf#509 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 72/100