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

Open
#23 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
5/5
Estimated time
Over a week
Newbie friendliness
35/100
Issue type
Feature
Clarity
Mostly clear
Activity status
Quiet
Tech stack
node.js, zig
Domain
api, backend

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:

  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:

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.

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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from ChainSafe/zapi

All issues in ChainSafe/zapi

Similar issues

More Backend & API Design issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.