Skip to content

Add opt-in binding metadata for API documentation - #233

Open
Segfaultd wants to merge 4 commits into
pmed:masterfrom
MafiaHub:upstream/native-scripting-api-metadata
Open

Add opt-in binding metadata for API documentation#233
Segfaultd wants to merge 4 commits into
pmed:masterfrom
MafiaHub:upstream/native-scripting-api-metadata

Conversation

@Segfaultd

@Segfaultd Segfaultd commented Jul 16, 2026

Copy link
Copy Markdown

Why

Embedders often need scripting API documentation, TypeScript declarations, or other tooling derived from their V8 bindings. Today that generally requires a second, manually maintained description of the API. That duplicate source drifts from the runtime, and raw V8 callbacks do not expose enough signature or documentation information to reconstruct the intended JavaScript API afterward.

This PR adds an optional, renderer-neutral metadata layer so a binding and its public API description can be declared together. Existing v8pp binding APIs remain available and unchanged; metadata is recorded only when a registry-backed module or class is used.

What this adds

  • metadata registries and named catalogs for isolated client/server or per-module APIs
  • explicit runtime symbol shapes: global objects, constructors, and structural data types
  • functions, constructor call signatures, parameters, return types, properties, variables, inheritance, descriptions, examples, since, and deprecation metadata
  • C++ signature inference for typed callables and class_::ctor<Args...>(), with optional constructor documentation and explicit JavaScript signatures for raw V8 callbacks or native/JS type mismatches
  • metadata-aware v8pp::module and v8pp::class_ overloads that install the runtime binding and record its documentation atomically
  • support for raw accessors, prototype/static callbacks, globals, getter-backed globals, constants, and concrete object values on materialized module instances
  • JSON schema-v2 serialization plus file and environment-controlled catalog export
  • deduplication and validation to keep recorded metadata aligned with the actual runtime shape and read-only/static semantics

Documented static values are installed on the constructor FunctionTemplate as accessors backed by v8pp-managed external storage. This avoids prematurely materializing constructors, supports object-valued and writable statics, and works whether a class is published directly or through module::class_().

Additional fix: read-write property setters through inherited prototypes

class_::property(name, get, set) installed the accessor with SetNativeDataProperty, whose setter does not fire when the property is assigned on a derived instance through an inherited prototype: V8 creates an own data property on the receiver instead of invoking the ancestor setter. Getters walk the prototype chain correctly, so only setters were affected, and only on classes that inherit<Base>() a base which declares a read-write property.

This PR now registers accessor-compatible read-write properties as real JS accessors via SetAccessorProperty, whose setter runs through the prototype chain. Read-only properties and direct callback-form getters/setters (those taking a v8::PropertyCallbackInfo) keep the SetNativeDataProperty path, so behavior for non-inherited and read-only properties is unchanged.

struct Base { glm::vec3 pos; };
struct Derived : Base {};

v8pp::class_<Base> base(isolate);
base.property("position", &get_pos, &set_pos);

v8pp::class_<Derived> derived(isolate);
derived.inherit<Base>();

// derivedInstance.position = v now invokes set_pos (previously silently created an own property)

Compatibility

  • The feature is opt-in and existing undocumented bindings continue to behave as before.
  • The project/package version is deliberately unchanged; release versioning remains with the maintainers.
  • The header-only CMake definition now uses #cmakedefine01, fixing the existing ON/OFF preprocessor mismatch exposed by the supported CI matrix.
  • The workflow uses explicit triggers, read-only permissions, non-persisted checkout credentials, and a current checkout action.

Validation

  • compiled static library: native tests pass
  • compiled shared library: native and JavaScript plugin tests pass
  • header-only, plugins disabled: native tests pass
  • header-only, plugins enabled: native and JavaScript plugin tests pass
  • constructor metadata serialization and documented class_::ctor behavior are covered by native tests
  • all four supported local build variants were rerun after the constructor metadata update
  • actionlint passes for the updated workflow
  • validated in a downstream V8/Node embedder with generated server/client API documentation and runtime metadata export
  • the inherited-prototype setter fix validated in a downstream embedder: assigning a base-declared read-write property on a derived handle now invokes the C++ setter (previously a no-op)

Example

auto& api = v8pp::metadata::catalog("my-mod-client");
v8pp::module camera(isolate, api, "Camera", "Client camera helpers");

camera.function("worldToScreen", &world_to_screen_callback,
    v8pp::metadata::docs("ScreenPosition", {
        v8pp::metadata::param("x", "number"),
        v8pp::metadata::param("y", "number"),
        v8pp::metadata::param("z", "number"),
    }, "Projects a world position onto the screen"));

camera.publish(global);

The exported JSON is intentionally presentation-agnostic so downstream projects can produce HTML, declarations, editor data, or other formats without coupling those generators to v8pp.

* feat: add native binding metadata

* test: expand native metadata coverage

* fix: align metadata with runtime bindings

* feat: streamline documented bindings

* fix: restore header-only metadata builds

* fix: align documented property runtime shapes
class_::property(get, set) installed the accessor with SetNativeDataProperty,
whose setter does not fire when the property is assigned on a derived instance
through an inherited prototype -- V8 creates an own data property on the
receiver instead of invoking the ancestor setter. Getters walk the chain fine,
so only setters on derived types were affected.

Register read-write properties as real JS accessors (SetAccessorProperty), whose
setter runs through the prototype chain. Read-only properties and direct
callback-form properties (that take a v8::PropertyCallbackInfo) keep the
SetNativeDataProperty path.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant