Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions CODE_ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ The compatibility split is intentional:
- Go and rust-native already use core structural decoding.
- Python and Node use native graph adapters that preserve host object identity
and cycles.
- Java, C#/.NET, Ruby, and PHP retain full structural copies until the
remaining C-ABI binding migration.
- Java, C#/.NET, Ruby, and PHP use the opaque C graph API. Their shims only
allocate host values, connect graph edges, and map leaf values.

The rule, applied to every change:

Expand Down Expand Up @@ -116,15 +116,15 @@ The core graph parser establishes the evaluate-wire and launch-parser ownership
| Python | PyO3 native graph adapter for evaluate results; Playwright-shaped validation and ergonomics remain in Python. |
| Go | C ABI binding that already uses the core's flattened structural decoder and maps leaf values. |
| rust-native | In-process facade that already uses core structural decoding and maps leaf values. |
| C ABI | Legacy flattened JSON decoder and core launch parser; it does not preserve host graph identity. |
| Java / C#/.NET / Ruby / PHP | C-ABI bindings with full structural copies pending migration to the core decoder. |
| C ABI | Legacy flattened JSON decoder plus an opaque identity-preserving graph API; both use the core parser. |
| Java / C#/.NET / Ruby / PHP | Two-pass host materializers over the opaque C graph API; leaf conversion stays language-native. |
| Node | napi native graph adapter; the typed camelCase coercion veneer remains in Node. |

The remaining cross-shim concerns have these owners:

| Concern | Current owner | Remaining work |
| --- | --- | --- |
| Evaluate wire | Core parser; C ABI uses its flattened compatibility decoder, while Python and Node use native graph adapters. | Migrate Java, C#/.NET, Ruby, and PHP to the core decoder. |
| Evaluate wire | Core parser; C ABI exposes flattened compatibility and opaque graph paths. Every identity-preserving binding materializes the core graph. | Keep only language-native leaf conversion in binding shims. |
| Launch options | Core parser accepts canonical snake_case and current camelCase aliases. | Keep C callers on canonical snake_case; retain language-specific validation and coercion only where required for compatibility. |
| Screenshot options | Core parser. | Keep language veneers limited to marshalling and native coercion. |
| Data URL / JSON equality / manifest validation | Binding harnesses. | Move behind C-ABI helpers when harness work next opens. |
Expand All @@ -138,9 +138,9 @@ Ordered tracks; each is independently landable and keeps parity tests green.
legacy C ABI decoder flattened for compatibility. Use native graph adapters
in Python and Node, and retain core structural decoding in Go and
rust-native.
2. **Migrate the remaining C-ABI bindings.** Move Java, C#/.NET, Ruby, and PHP
from full structural copies to the core graph contract. This remains
pending; do not treat those migrations as complete.
2. **Expose the graph through the C ABI (this change).** Java, C#/.NET, Ruby,
and PHP materialize the opaque core graph instead of parsing wire wrappers,
tags, and references in each language.
3. **Maintain the generated async Python facade.** Keep mechanical delegation
methods generated from `sync_api.py` signatures, keep hand-written code
limited to methods with real async semantics, and keep the checked-in
Expand Down Expand Up @@ -281,7 +281,8 @@ Chrome-for-Testing installer is linux x86_64-only.

1. Preserve and extend the core evaluate-wire and launch-parser contract
without changing the legacy C ABI behavior.
2. Migrate the remaining C-ABI bindings; this is not complete.
2. Keep every language binding limited to graph materialization, native leaf
conversion, ownership, and public validation.
3. Maintain the generated async Python facade and its ownership checks.
4. Extract Python event waiters/context managers into
`python/rustwright/events.py` (pairs with roadmap track 7).
Expand Down
99 changes: 95 additions & 4 deletions bindings/CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,28 @@ Opaque handles have no public layout:
```c
typedef struct RwBrowser RwBrowser;
typedef struct RwPage RwPage;
typedef struct RwWireGraph RwWireGraph;
typedef size_t RwWireNodeId;
typedef int32_t RwWireNodeKind;
#define RW_WIRE_NODE_NULL 0
#define RW_WIRE_NODE_BOOL 1
#define RW_WIRE_NODE_SIGNED 2
#define RW_WIRE_NODE_UNSIGNED 3
#define RW_WIRE_NODE_FLOAT 4
#define RW_WIRE_NODE_STRING 5
#define RW_WIRE_NODE_ARRAY 6
#define RW_WIRE_NODE_OBJECT 7
#define RW_WIRE_NODE_LEAF 8
typedef int32_t RwWireLeafKind;
#define RW_WIRE_LEAF_UNSERIALIZABLE 0
#define RW_WIRE_LEAF_BIGINT 1
#define RW_WIRE_LEAF_DATE 2
#define RW_WIRE_LEAF_REGEXP 3
#define RW_WIRE_LEAF_URL 4
#define RW_WIRE_LEAF_ERROR 5
#define RW_WIRE_LEAF_UNDEFINED 6
#define RW_WIRE_LEAF_SYMBOL 7
#define RW_WIRE_LEAF_FUNCTION 8
```

The complete exported ABI is:
Expand All @@ -51,6 +73,63 @@ void rw_bytes_free(uint8_t *buf, size_t len);

/* evaluate wire */
int32_t rw_decode_wire(const char *wire_json, char **out_json);
int32_t rw_wire_graph_parse(const char *wire_json,
RwWireGraph **out_graph);
void rw_wire_graph_free(RwWireGraph *graph);
int32_t rw_wire_graph_node_count(const RwWireGraph *graph,
size_t *out_count);
int32_t rw_wire_graph_root(const RwWireGraph *graph,
RwWireNodeId *out_root);
int32_t rw_wire_graph_node_kind(const RwWireGraph *graph,
RwWireNodeId node,
RwWireNodeKind *out_kind);
int32_t rw_wire_graph_get_bool(const RwWireGraph *graph,
RwWireNodeId node,
int32_t *out_value);
int32_t rw_wire_graph_get_signed(const RwWireGraph *graph,
RwWireNodeId node,
int64_t *out_value);
int32_t rw_wire_graph_get_unsigned(const RwWireGraph *graph,
RwWireNodeId node,
uint64_t *out_value);
int32_t rw_wire_graph_get_float(const RwWireGraph *graph,
RwWireNodeId node,
double *out_value);
int32_t rw_wire_graph_get_string(const RwWireGraph *graph,
RwWireNodeId node,
const uint8_t **out_data,
size_t *out_len);
int32_t rw_wire_graph_array_length(const RwWireGraph *graph,
RwWireNodeId node,
size_t *out_len);
int32_t rw_wire_graph_array_child(const RwWireGraph *graph,
RwWireNodeId node,
size_t index,
RwWireNodeId *out_child);
int32_t rw_wire_graph_object_length(const RwWireGraph *graph,
RwWireNodeId node,
size_t *out_len);
int32_t rw_wire_graph_object_key(const RwWireGraph *graph,
RwWireNodeId node,
size_t index,
const uint8_t **out_data,
size_t *out_len);
int32_t rw_wire_graph_object_child(const RwWireGraph *graph,
RwWireNodeId node,
size_t index,
RwWireNodeId *out_child);
int32_t rw_wire_graph_leaf_kind(const RwWireGraph *graph,
RwWireNodeId node,
RwWireLeafKind *out_kind);
int32_t rw_wire_graph_leaf_field_count(const RwWireGraph *graph,
RwWireNodeId node,
size_t *out_count);
int32_t rw_wire_graph_leaf_field(const RwWireGraph *graph,
RwWireNodeId node,
size_t index,
const uint8_t **out_data,
size_t *out_len);


/* Chromium */
int32_t rw_chromium_executable_path(char **out_path);
Expand Down Expand Up @@ -173,10 +252,11 @@ non-cyclic references are duplicated, and references that form cycles become
`{"__rustwright_cdp_cycle__": true}`. Leaf scalar tags remain in the output for
binding-specific native mapping. This behavior is compatibility-stable.

The PyO3 and napi graph adapters use the core graph representation instead.
They allocate host arrays and objects with stable identity, preserve repeated
references and cycles, and keep leaf conversion policy in the adapter. These
graph adapters must not be replaced with the legacy flattened C ABI decoder.
Identity-preserving bindings use `rw_wire_graph_parse`. They allocate all
host containers by dense node id before they fill array and object edges.
This preserves repeated references, cycles, and object entry order. PyO3 and
napi use the same core graph directly. None of these adapters may substitute
the flattened `rw_decode_wire` compatibility path.

The core serializer is the single source of truth for this vocabulary; a
binding maps the core's tags to its closest native representation and must not
Expand All @@ -185,6 +265,17 @@ values are JSON-compatible and never require cycles; a runner must at least
recursively decode array/object wrappers before capture or `assertEval`
comparison.

`RwWireGraph` is immutable and caller-owned. Borrowed string, object-key, and
leaf-field byte views remain valid only until `rw_wire_graph_free`. Bindings
must copy each view by its explicit length before graph release. The length can
include embedded NUL bytes. Empty views use NULL plus zero.

Node and leaf tag values are fixed in `capi/include/rustwright.h`, where both
public kind aliases are `int32_t` rather than implementation-defined C enums.
Leaf fields are positional: unserializable, bigint, date, and URL have one
field; regexp has pattern then flags; error has name, message, then stack;
undefined, symbol, and function have no fields.

### Thin-shim rule (single source of logic)

Any behavior expressible as a pure function of JSON-in/JSON-out — launch and
Expand Down
5 changes: 5 additions & 0 deletions capi/examples/smoke.c
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
#include <stdio.h>
#include <stdlib.h>

_Static_assert(sizeof(RwWireNodeKind) == 4,
"RwWireNodeKind must remain a 32-bit ABI type");
_Static_assert(sizeof(RwWireLeafKind) == 4,
"RwWireLeafKind must remain a 32-bit ABI type");

static void fail(const char *operation) {
const char *error = rw_last_error();
fprintf(stderr, "%s failed: %s\n", operation, error ? error : "unknown error");
Expand Down
136 changes: 136 additions & 0 deletions capi/include/rustwright.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,38 @@ typedef struct RwBrowser RwBrowser;
/** Opaque page handle owned by the caller. */
typedef struct RwPage RwPage;

/** Opaque immutable parsed wire graph owned by the caller. */
typedef struct RwWireGraph RwWireGraph;

/** Dense node ids are indices into the immutable graph. */
typedef size_t RwWireNodeId;

/*
* These tags use a fixed-width integer type. C enum storage is implementation
* defined and may not match the Rust ABI when passed through a pointer.
*/
typedef int32_t RwWireNodeKind;
#define RW_WIRE_NODE_NULL 0
#define RW_WIRE_NODE_BOOL 1
#define RW_WIRE_NODE_SIGNED 2
#define RW_WIRE_NODE_UNSIGNED 3
#define RW_WIRE_NODE_FLOAT 4
#define RW_WIRE_NODE_STRING 5
#define RW_WIRE_NODE_ARRAY 6
#define RW_WIRE_NODE_OBJECT 7
#define RW_WIRE_NODE_LEAF 8

typedef int32_t RwWireLeafKind;
#define RW_WIRE_LEAF_UNSERIALIZABLE 0
#define RW_WIRE_LEAF_BIGINT 1
#define RW_WIRE_LEAF_DATE 2
#define RW_WIRE_LEAF_REGEXP 3
#define RW_WIRE_LEAF_URL 4
#define RW_WIRE_LEAF_ERROR 5
#define RW_WIRE_LEAF_UNDEFINED 6
#define RW_WIRE_LEAF_SYMBOL 7
#define RW_WIRE_LEAF_FUNCTION 8

/**
* Return the current thread's last error as borrowed UTF-8.
*
Expand Down Expand Up @@ -44,6 +76,110 @@ void rw_bytes_free(uint8_t *buf, size_t len);
*/
int32_t rw_decode_wire(const char *wire_json, char **out_json);

/**
* Parse the evaluate wire format into an immutable graph.
*
* The graph owns all nodes and strings. On success, free it with
* rw_wire_graph_free. On failure, `*out_graph` is NULL.
*/
int32_t rw_wire_graph_parse(const char *wire_json, RwWireGraph **out_graph);

/** Release a graph and all borrowed views into it. NULL is accepted. */
void rw_wire_graph_free(RwWireGraph *graph);

/** Return the dense number of nodes in a graph. */
int32_t rw_wire_graph_node_count(const RwWireGraph *graph, size_t *out_count);

/** Return the graph root's dense node id. */
int32_t rw_wire_graph_root(const RwWireGraph *graph, RwWireNodeId *out_root);

/** Return a node's kind. */
int32_t rw_wire_graph_node_kind(const RwWireGraph *graph,
RwWireNodeId node,
RwWireNodeKind *out_kind);

/** Read a boolean node as 0 or 1. */
int32_t rw_wire_graph_get_bool(const RwWireGraph *graph,
RwWireNodeId node,
int32_t *out_value);

/** Read a signed integer node. */
int32_t rw_wire_graph_get_signed(const RwWireGraph *graph,
RwWireNodeId node,
int64_t *out_value);

/** Read an unsigned integer node. */
int32_t rw_wire_graph_get_unsigned(const RwWireGraph *graph,
RwWireNodeId node,
uint64_t *out_value);

/** Read a floating-point node. */
int32_t rw_wire_graph_get_float(const RwWireGraph *graph,
RwWireNodeId node,
double *out_value);

/**
* Read a string node as a borrowed byte view.
*
* The view includes embedded NUL bytes and remains valid until graph free.
* Empty strings return NULL and zero.
*/
int32_t rw_wire_graph_get_string(const RwWireGraph *graph,
RwWireNodeId node,
const uint8_t **out_data,
size_t *out_len);

/** Return an array node's length. */
int32_t rw_wire_graph_array_length(const RwWireGraph *graph,
RwWireNodeId node,
size_t *out_len);

/** Return an array child node id in native order. */
int32_t rw_wire_graph_array_child(const RwWireGraph *graph,
RwWireNodeId node,
size_t index,
RwWireNodeId *out_child);

/** Return an object node's entry count. */
int32_t rw_wire_graph_object_length(const RwWireGraph *graph,
RwWireNodeId node,
size_t *out_len);

/** Return an object key as a borrowed byte view in native order. */
int32_t rw_wire_graph_object_key(const RwWireGraph *graph,
RwWireNodeId node,
size_t index,
const uint8_t **out_data,
size_t *out_len);

/** Return an object child node id in native order. */
int32_t rw_wire_graph_object_child(const RwWireGraph *graph,
RwWireNodeId node,
size_t index,
RwWireNodeId *out_child);

/** Return a leaf's canonical kind. */
int32_t rw_wire_graph_leaf_kind(const RwWireGraph *graph,
RwWireNodeId node,
RwWireLeafKind *out_kind);

/**
* Return the positional field count for a leaf.
*
* Unserializable, bigint, date, and URL have one field; regexp has two;
* error has three; undefined, symbol, and function have zero.
*/
int32_t rw_wire_graph_leaf_field_count(const RwWireGraph *graph,
RwWireNodeId node,
size_t *out_count);

/** Return one positional leaf field as a borrowed byte view. */
int32_t rw_wire_graph_leaf_field(const RwWireGraph *graph,
RwWireNodeId node,
size_t index,
const uint8_t **out_data,
size_t *out_len);

/**
* Discover Chromium and return its executable path.
*
Expand Down
Loading
Loading