(If that means nothing to you, neither will the rest of this page!)
+To the best of its creator's fallible knowledge, Jaccwabyt is the only
+library of its kind (as of 2025-11). Aside from wrapping existing
+structs, e.g. to integrate "legacy" C code into JS/WASM, it can also
+model C structs without requiring a native C counterpart, a feature
+which probably has only exceedingly obscure uses in JS-side
+implementations for native callbacks.
+
+How it works:
+
+- The client provides a JSON-friendly description of a C struct,
+ describing the names, sizes, and offsets of each member.
+- Pass that description to a factory function to create
+ a JS constructor for that C struct.
+- That constructor allocates a block of heap memory of the C struct's
+ size and maps it to the new JS-side struct instance. Each instance
+ inherits property interceptors for each struct member, such that
+ fetching the C struct's members reads directly from the struct's
+ memory and setting them writes to that memory. Similarly, these
+ objects can be provided with memory constructed elsewhere, e.g.
+ a struct pointer returned from a WASM function, and can proxy
+ that memory via the struct's interface.
+- Clients eventually call the `dispose()` method to free the
+ instance's heap memory and disassociate the JS instance with its
+ WASM-side resources.
+
+Easy peasy!
+
+**Build instructions**: [see Appendix B](#appendix-b).
+
**Browser compatibility**: this library requires a _recent_ browser
and makes no attempt whatsoever to accommodate "older" or
lesser-capable ones, where "recent," _very roughly_, means released in
**Non-browser compatibility**: this code does not target non-browser
JS engines and is completely untested on them. That said, it "might
-work".
+work". These JS APIs do not use the DOM API in any way, so are not
+specifically tied to a browser, but they _are_ fully untested in such
+environments. This code is known to work with both [Emscripten][] builds
+and [WASI-SDK][] SDK builds (at of this writing, 2025-11-08).
**64-bit WASM:** as of 2025-09-21 this API supports 64-bit WASM builds
but it has to be configured for it (see [](#api-binderfactory) for
-----
-<a name='overview'></a>
+<a id='overview'></a>
Table of Contents
============================================================
- APIs
- [Struct Binder Factory](#api-binderfactory)
- [Struct Binder](#api-structbinder)
- - [Struct Type](#api-structtype)
+ - [Struct Description Objects](#struct-descr)
+- [Struct Type](#api-structtype)
- [Struct Constructors](#api-structctor)
- [Struct Protypes](#api-structprototype)
- [Struct Instances](#api-structinstance)
- Appendices
- [Appendix A: Limitations, TODOs, etc.](#appendix-a)
+ - [Appendix B: Build](#appendix-b)
- [Appendix D: Debug Info](#appendix-d)
- [Appendix G: Generating Struct Descriptions](#appendix-g)
-<a name='overview'></a>
+<a id='overview'></a>
Overview
============================================================
because it is the most widespread WASM toolchain, but this code is
specifically designed to be usable in arbitrary WASM environments.
It abstracts away a few Emscripten-specific features into
- configurable options. Similarly, the build tree requires Emscripten
- but Jaccwabyt does not have any hard Emscripten dependencies.
+ configurable options. The build tree supports both [Emscripten][]
+ and [WASI-SDK][] to demonstrate that it has no dependencies on
+ either.
- This code is encapsulated into a single JavaScript function. It
should be trivial to copy/paste into arbitrary WASM/JS-using
projects.
- The source tree includes C code, but only for testing and
- demonstration purposes. It is not part of the core distributable.
+ demonstration purposes. It is not a core distributable.
-<a name='architecture'></a>
+<a id='architecture'></a>
Architecture
------------------------------------------------------------
an appropriate configuration, to generate a single...
- **[StructBinder][]** is a factory function which converts an
arbitrary number struct descriptions into...
-- **[StructTypes][StructCtors]** are constructors, one per struct
+- **[StructType][]** are [constructors][StructCtor], one per struct
description, which inherit from
- **[`StructBinder.StructType`][StructType]** and are used to instantiate...
+ **[`StructBinder.StructType`][StructType]** and are used to
+ instantiate...
- **[Struct instances][StructInstance]** are objects representing
individual instances of generated struct types.
namespace for struct creation.
-<a name='creating-binding'></a>
+<a id='creating-binding'></a>
Creating and Binding Structs
============================================================
Detailed instructions for each of those steps follows...
-<a name='step-1'></a>
+<a id='step-1'></a>
Step 1: Configure Jaccwabyt for the Environment
------------------------------------------------------------
a Uint8Array or Int8Array view of the WASM memory,
alloc: function(howMuchMemory){...},
dealloc: function(pointerToFree){...},
- pointerIR: 'i32' or 'i64' // WASM pointer type - default = 'i32'
+ pointerSize: 4 or 8 // WASM pointer size
});
```
The StructBinder factory function returns a function which can then be
used to create bindings for our structs.
-<a name='step-2'></a>
+<a id='step-2'></a>
Step 2: Create a Struct Description
------------------------------------------------------------
}
```
-These data _must_ match up with the C-side definition of the struct
-(if any). See [Appendix G][appendix-g] for one way to easily generate
-these from C code.
-
-Each entry in the `members` object maps the member's name to
-its low-level layout:
-
-- `offset`: the byte offset from the start of the struct, as reported
- by C's `offsetof()` feature.
-- `sizeof`: as reported by C's `sizeof()`.
-- `signature`: described below.
-- `readOnly`: optional. If set to true, the binding layer will
- throw if JS code tries to set that property.
-
-The order of the `members` entries is not important: their memory
-layout is determined by their `offset` and `sizeof` members. The
-`name` property is technically optional, but one of the steps in the
-binding process requires that either it be passed an explicit name or
-there be one in the struct description. The names of the `members`
-entries need not match their C counterparts. Project conventions may
-call for giving them different names in the JS side and the
-[StructBinderFactory][] can be configured to automatically add a
-prefix and/or suffix to their names.
-
-Nested structs are as-yet unsupported by this tool.
+This is described in more detail in [][StructBinder].
-Struct member "signatures" describe the data types of the members and
-are an extended variant of the format used by Emscripten's
-`addFunction()`. A signature for a non-function-pointer member, or
-function pointer member which is to be modelled as an opaque pointer,
-is a single letter. A signature for a function pointer may also be
-modelled as a series of letters describing the call signature. The
-supported letters are:
-
-- **`v`** = `void` (only used as return type for function pointer members)
-- **`i`** = `int32` (4 bytes)
-- **`j`** = `int64` (8 bytes) is only really usable if this code is built
- with BigInt support (e.g. using the Emscripten `-sWASM_BIGINT` build
- flag). Without that, this API may throw when encountering the `j`
- signature entry.
-- **`f`** = `float` (4 bytes)
-- **`d`** = `double` (8 bytes)
-- **`c`** = `int8` (1 byte) char - see notes below!
-- **`C`** = `uint8` (1 byte) unsigned char - see notes below!
-- **`p`** = `int32` (see notes below!)
-- **`P`** = Like `p` but with extra handling. Described below.
-- **`s`** = like `int32` but is a _hint_ that it's a pointer to a
- string so that _some_ (very limited) contexts may treat it as such,
- noting that such algorithms must, for lack of information to the
- contrary, assume both that the encoding is UTF-8 and that the
- pointer's member is NUL-terminated. If that is _not_ the case for a
- given string member, do not use `s`: use `i` or `p` instead and do
- any string handling yourself.
-
-Noting that:
-
-- **All of these types are numeric**. Attempting to set any
- struct-bound property to a non-numeric value will trigger an
- exception except in cases explicitly noted otherwise.
-- **"Char" types**: WASM does not define an `int8` type, nor does its
- JS representation distinguish between signed and unsigned. This API
- treats `c` as `int8` and `C` as `uint8` for purposes of getting and
- setting values when using the `DataView` class. It is _not_
- recommended that client code use these types in new WASM-capable
- code, but they were added for the sake of binding some immutable
- legacy code to WASM.
-
-> Sidebar: Emscripten's public docs do not mention `p`, but their
-generated code includes `p` as an alias for `i`, presumably to mean
-"pointer". Though `i` is legal for pointer types in the signature, `p`
-is more descriptive, so this framework encourages the use of `p` for
-pointer-type members. Using `p` for pointers also helps future-proof
-the signatures against the eventuality that WASM eventually supports
-64-bit pointers. Note that sometimes `p` _really_ means a
-pointer-to-pointer. We simply have to be aware of when we need to deal
-with pointers and pointers-to-pointers in JS code.
-
-> Trivia: this API treates `p` as distinctly different from `i` in
-some contexts, so its use is encouraged for pointer types.
-
-Signatures in the form `x(...)` denote function-pointer members and
-`x` denotes non-function members. Functions with no arguments use the
-form `x()`. For function-type signatures, the strings are formulated
-such that they can be passed to Emscripten's `addFunction()` after
-stripping out the `(` and `)` characters. For good measure, to match
-the public Emscripten docs, `p`, `c`, and `C`, should also be replaced
-with `i`. In JavaScript that might look like:
-
->
-```
-signature.replace(/[^vipPsjfdcC]/g,'').replace(/[pPscC]/g,'i');
-```
-
-<a name='step-2-pvsp'></a>
+<a id='step-2-pvsp'></a>
### `P` vs `p` in Method Signatures
*This support is experimental and subject to change.*
or `P` is used).
-<a name='step-3'></a>
+<a id='step-3'></a>
Step 3: Binding the Struct
------------------------------------------------------------
it a copy of the original, e.g. by passing it
`JSON.parse(JSON.stringify(structDefinition))`.
-<a name='step-4'></a>
+<a id='step-4'></a>
Step 4: Creating, Using, and Destroying Struct Instances
------------------------------------------------------------
can do with them, as covered in the rest of this document.
-<a name='api'></a>
+<a id='api'></a>
API Reference
============================================================
-<a name='api-binderfactory'></a>
+<a id='api-binderfactory'></a>
API: Binder Factory
------------------------------------------------------------
It returns a function which these docs refer to as a [StructBinder][]
(covered in the next section). It throws on error.
-The binder factory supports the following options in its
-configuration object argument:
+The binder factory supports the following options in its configuration
+object argument:
-- `pointerIR` (Added 2025-09-21)
- Optionally specify the WASM pointer size with the string `'i32'` or
- `'i64'`, defaulting to the former. When using with 64-bit WASM
- builds, this must be set to `'i64'` by the client. Any other value
- triggers an exception.
+- `pointerSize` (Added 2025-11-15 to replace `pointerIR`)
+ Optionally specify the WASM pointer size of 4 (32-bit) or 8
+ (64-bit). Any other truthy value triggers an exception. If
+ `pointerSize` is not set then it will guess the size by `alloc()`ing
+ one byte, checking the result type, and `dealloc()`ing it.
- `heap`
Must be either a `WebAssembly.Memory` instance representing the WASM
for the WASM heap to grow at runtime.
- `alloc`
- Must be a function semantically compatible with Emscripten's
- `Module._malloc()`. That is, it is passed the number of bytes to
- allocate and it returns a pointer. On allocation failure it may
- either return 0 or throw an exception. This API will throw an
- exception if allocation fails or will propagate whatever exception
- the allocator throws. The allocator _must_ use the same heap as the
- `heap` config option.
+ Must be a function semantically compatible with C's
+ `malloc(3)`. That is, it is passed the number of bytes to allocate
+ and it returns a pointer. On allocation failure it may either return
+ 0 or throw an exception. This API will throw an exception if
+ allocation fails or will propagate whatever exception the allocator
+ throws. The allocator _must_ use the same heap as the `heap` config
+ option.
- `dealloc`
- Must be a function semantically compatible with Emscripten's
- `Module._free()`. That is, it takes a pointer returned from
- `alloc()` and releases that memory. It must never throw and must
- accept a value of 0/null to mean "do nothing" (noting that 0 is
- _technically_ a legal memory address in WASM, but that seems like a
- design flaw).
+ Must be a function semantically compatible with C's `free(3)`. That
+ is, it takes a pointer returned from `alloc()` and releases that
+ memory. It must never throw and must accept a value of 0/null to
+ mean "do nothing".
+
+- `realloc`
+ Optional but required for (eventual (and optional) realloc support
+ of structs. If set, it must be a function semantically compatible
+ with C's `realloc()`. See `alloc`, above, for other requirements.
- `bigIntEnabled` (bool=true if BigInt64Array is available, else false)
If true, the WASM bits this code is used with must have been
(like `console.debug` does). See [Appendix D](#appendix-d) for info
about enabling debugging output.
-<a name='api-structbinder'></a>
+<a id='api-structbinder'></a>
API: Struct Binder
------------------------------------------------------------
Struct Binders are factories which are created by the
[StructBinderFactory][]. A given Struct Binder can process any number
-of distinct structs. In a typical setup, an app will have ony one
+of distinct structs. In a typical setup, an app will have only one
shared Binder Factory and one Struct Binder. Struct Binders which are
created via different [StructBinderFactory][] calls are unrelated to each
other, sharing no state except, perhaps, indirectly via
described by its argument(s), each of which derives from
a separate [StructType][] instance.
-The Struct Binder has the following members:
+StructBinder has the following members:
- `allocCString(str)`
Allocates a new UTF-8-encoded, NUL-terminated copy of the given JS
any of its "significant" configuration values may have undefined
results.
-<a name='api-structtype'></a>
+- `adaptGet(key [,func])`
+ Gets or sets a "get adaptor" by name - an arbitrary client-defined
+ string, e.g. `"to-js-string"`. Struct description objects may have
+ their `adaptGet` property set to the name of a mapped getter to
+ behave exactly as if that struct description had set the given
+ function as its `get` property. This offers a JSON-friendly way of
+ storing adaptor mappings, with the caveat that the adaptors need to
+ be defined _somewhere_ outside of JSON (typically it should be done
+ immediately after creating the StructBinder).
+
+- `adaptSet(key [,func])`
+ The "set" counterpart of `adaptGet`.
+
+- `ptrAdd(...)`
+ Coerces all of its arguments to the WASM pointer type, adds them
+ together, and returns a result of that same type. This is a
+ workaround for mixed-BigInt/Number pointer math being illegal in JS.
+
+The `structDescription` argument is described in detail in the
+following section.
+
+<a id='struct-descr'></a>
+### Struct Description Object
+
+C structs are described in a JSON-friendly format:
+
+>
+```json
+{
+ "name": "MyStruct",
+ "sizeof": 16,
+ "members": {
+ "member1": {"offset": 0,"sizeof": 4,"signature": "i"},
+ "member2": {"offset": 4,"sizeof": 4,"signature": "p"},
+ "member3": {"offset": 8,"sizeof": 8,"signature": "j"}
+ }
+}
+```
+
+Forewarning: these data _must_ match up with the C-side definition of
+the struct (if any). See [Appendix G][appendix-g] for one way to
+easily generate these from C code.
+
+Every struct must have a `sizeof`. (Though we _could_ calculate it
+based on the list of members, we don't. Actually, we do, then we throw
+if the values don't match up.) The `name` is required as well but it
+may optionally be passed as the first argument to
+`StructBinder(structName,structDescription)`. the `name` property
+represents the member's symbolic name, typically its native name.
+
+Abstractly speaking, a struct description in an object with the
+properties `sizeof`, `offset`, and either `signature` _or_
+`member`. `offset` is optional only in the top-most object of a struct
+description. Every sub-object (a.k.a. member description object)
+requires the `offset` property.
+
+Member description objects are those in the `members` property:
+
+`"members": {"memberName": {...member description...}, ...}`
+
+A struct description which has its own `members` object represents a
+nested struct, with an identical description syntax to that of a
+top-level struct except that nested structs require an `offset`
+property.
+
+Each entry in a struct/member description object maps the member's
+name to its low-level layout and other metadata:
+
+- `offset`
+ The byte offset from the start of the struct, as reported by C's
+ `offsetof()` feature. For nested structs's members, this value is
+ relative to the nested struct, not the parent struct.
+- `sizeof`
+ As reported by C's `sizeof()`.
+- `signature`
+ A type-id signature for this member. Described below.
+- `readOnly [=false]`
+ Optional boolean. If set to true, the binding layer will throw if JS
+ code tries to set that property.
+- `zeroOnDispose [=false]`
+ If true, then the library will zero out the memory of instances of
+ this struct when their `dispose()` method is called. Because
+ `StructType.dispose()` does not free instances which wrap
+ externally-provided memory, those instances are not wiped when
+ disposed (doing so would likely interfere with other users of that
+ memory). (There is no need for a `zeroOnAlloc` counterpart because
+ newly-allocated instances are always zero-filled for sanity's
+ sake.)
+- `members`
+ This object describes the individual struct members, mapping their
+ names to a member description object. `members` gets processed
+ recursively. Any member with its own `members` property is a
+ nested-struct, and the property accessor for such members will
+ return an instance of the distinct StructType which wraps that
+ member's native memory.
+ Nested-struct members cannot be assigned over. `signature` is
+ illegal if `members` is set.
+- `get`
+ Optional function. When fetching this member, this getter is passed
+ `(K,V)`, where `K` is the struct member's key and `V` is the
+ _native_ value. `get()`'s return value becomes the value of the
+ property access operation. This enables custom "native-to-JS"
+ conversions. If the member is a nested struct, the value passed to
+ the getter is a StructType proxy object which provides access to its
+ own members, a subset of the parent object's memory. In the context
+ of the getter call, "this" is the object upon which the get is being
+ performed.
+- `set`
+ Optional function. When setting this member, this setter is passed
+ `(K,V)`, where `K` is the struct member's key and `V` is the _JS_
+ value the property is being assigned to. The `set()` return value is
+ assigned to the _native_ struct member. Thus `set()` _must_ return
+ an appropriate numeric value and can perform "JS-to-native"
+ conversions. `set` is not currently legal for nested struct values,
+ but it is on their own non-nested-struct members. In the context of
+ the setter call, "this" is the object upon which the set is being
+ performed.
+- `adaptGet` and `adaptSet`
+ JSON-friendly variants of `get` and `set`. Each may be assigned a
+ string value, and each such string must be mapped with
+ `StructBinder.adaptGet(key,func)`
+ resp. `StructBinder.adaptSet(key,func)`. With that in place, these
+ behave like `get` resp. `set`.
+- `structName`
+ Optional descriptive name, possibly distinct from the `name`,
+ primarily used for nested structs. The intent is that this be some
+ form of the struct type's name, optionally with leading parts of
+ this object is a nested struct.
+- `name`
+ Is usually optional, and is always optional in `members` entries
+ because their name is conveniently derived from their containing
+ object. `name` must be provided only for the top-most struct. The
+ intent is that `name` maps to the member's property name and that
+ `structName` optionally be set for nested structs (it will be
+ derived from the name if it's not set).
+
+The order of the `members` entries is not important: their memory
+layout is determined by their `offset` and `sizeof` members. The
+`name` property is technically optional, but one of the steps in the
+binding process requires that either it be passed an explicit name or
+there be one in the struct description. The names of the `members`
+entries need not match their C counterparts. Project conventions may
+call for giving them different names in the JS side and the
+[StructBinderFactory][] can be configured to automatically add a
+prefix and/or suffix to their names.
+
+Struct member "signatures" describe the data types of the members and
+are an extended variant of the format used by Emscripten's
+`addFunction()`. A signature for a non-function-pointer member, or
+function pointer member which is to be modelled as an opaque pointer,
+is a single letter. A signature for a function pointer may also be
+modelled as a series of letters describing the call signature. The
+supported letters are:
+
+- **`v`** = `void` (only used as return type for function pointer members)
+- **`i`** = `int32` (4 bytes)
+- **`j`** = `int64` (8 bytes) is only really usable if this code is built
+ with BigInt support (e.g. using the Emscripten `-sWASM_BIGINT` build
+ flag). Without that, this API may throw when encountering the `j`
+ signature entry.
+- **`f`** = `float` (4 bytes)
+- **`d`** = `double` (8 bytes)
+- **`c`** = `int8` (1 byte) char - see notes below!
+- **`C`** = `uint8` (1 byte) unsigned char - see notes below!
+- **`p`** = `int32` (see notes below!)
+- **`P`** = Like `p` but with extra handling. Described below.
+- **`s`** = like `int32` but is a _hint_ that it's a pointer to a
+ string so that _some_ (very limited) contexts may treat it as such,
+ noting that such algorithms must, for lack of information to the
+ contrary, assume both that the encoding is UTF-8 and that the
+ pointer's member is NUL-terminated. If that is _not_ the case for a
+ given string member, do not use `s`: use `i` or `p` instead and do
+ any string handling yourself.
+
+Noting that:
+
+- **All of these types are numeric**. Attempting to set any
+ struct-bound property to a non-numeric value will trigger an
+ exception except in cases explicitly noted otherwise.
+- **"Char" types**: WASM does not define an `int8` type, nor does its
+ JS representation distinguish between signed and unsigned. This API
+ treats `c` as `int8` and `C` as `uint8` for purposes of getting and
+ setting values when using the `DataView` class. It is _not_
+ recommended that client code use these types in new WASM-capable
+ code, but they were added for the sake of binding some immutable
+ legacy code to WASM.
+
+> Sidebar: Emscripten's public docs do not mention `p`, but their
+generated code includes `p` as an alias for `i`, presumably to mean
+"pointer". Though `i` is legal for pointer types in the signature, `p`
+is more descriptive, so this framework encourages the use of `p` for
+pointer-type members. Using `p` for pointers also helps future-proof
+the signatures against the eventuality that WASM eventually supports
+64-bit pointers. Note that sometimes `p` _really_ means a
+pointer-to-pointer. We simply have to be aware of when we need to deal
+with pointers and pointers-to-pointers in JS code.
+
+> Trivia: this API treates `p` as distinctly different from `i` in
+some contexts, so its use is encouraged for pointer types.
+
+Signatures in the form `x(...)` denote function-pointer members and
+`x` denotes non-function members. Functions with no arguments use the
+form `x()`. For function-type signatures, the strings are formulated
+such that they can be passed to Emscripten's `addFunction()` after
+stripping out the `(` and `)` characters. For good measure, to match
+the public Emscripten docs, `p`, `c`, and `C`, should also be replaced
+with `i`. In JavaScript that might look like:
+
+>
+```
+signature.replace(/[^vipPsjfdcC]/g,'').replace(/[pPscC]/g,'i');
+```
+
+
+<a id='api-structtype'></a>
API: Struct Type
------------------------------------------------------------
The StructType constructor cannot be called from client code. It is
only called by the [StructBinder][]-generated
-[constructors][StructCtors]. The `StructBinder.StructType` object
+[constructors][StructCtor]. The `StructBinder.StructType` object
has the following "static" properties (^Which are accessible from
individual instances via `theInstance.constructor`.):
a function-typed `ondispose` property, this call replaces it with an
array and moves that function into the array. In all other cases,
`ondispose` is assumed to be an array and the argument(s) is/are
- appended to it. Returns `this`.
+ appended to it. Returns `this`. See `dispose()`, below, for where
+ this applies.
- `allocCString(str)`
Identical to the [StructBinder][] method of the same name.
- `hasExternalPointer(object)`
Returns true if the given object's `pointer` member refers to an
"external" object. That is the case when a pointer is passed to a
- [struct's constructor][StructCtors]. If true, the memory is owned by
+ [struct's constructor][StructCtor]. If true, the memory is owned by
someone other than the object and must outlive the object.
- `isA(value)`
The base StructType prototype has the following members, all of which
are inherited by [struct instances](#api-structinstance) and may only
-legally be called on concrete struct instances unless noted otherwise:
+legally be used with concrete struct instances unless noted otherwise:
- `dispose()`
Frees, if appropriate, the WASM-allocated memory which is allocated
cleans up the object, a leak in the WASM heap memory pool will result.
When `dispose()` is called, if the object has a property named `ondispose`
then it is treated as follows:
- - If it is a function, it is called with the struct object as its `this`.
- That method must not throw - if it does, the exception will be
- ignored.
+ - If it is a function, it is called with the struct object as its
+ `this`. That method must not throw - if it does, the exception
+ will be ignored.
- If it is an array, it may contain functions, pointers, other
- [StructType] instances, and/or JS strings. If an entry is a
+ [StructType][] instances, and/or JS strings. If an entry is a
function, it is called as described above. If it's a number, it's
assumed to be a pointer and is passed to the `dealloc()` function
configured for the parent [StructBinder][]. If it's a
supported primarily for use as debugging information.
- Some struct APIs will manipulate the `ondispose` member, creating
it as an array or converting it from a function to array as
- needed.
+ needed. Most simply, `addOnDispose()` is used to manipulate the
+ on-dispose data.
+
+- `extraBytes` (integer, read-only)
+ If this instance was allocated with the `extraBytes` option, this is
+ that value, else it is 0.
- `lookupMember(memberName,throwIfNotFound=true)`
Given the name of a mapped struct member, it returns the member
the struct will invalidate older serialized data and (B) serializing
member pointers is useless.
+- `pointer` (number, read-only)
+ A read-only numeric property which is the "pointer" returned by the
+ configured allocator when this object is constructed. After
+ `dispose()` (inherited from [StructType][]) is called, this property
+ has the `undefined` value. When calling C-side code which takes a
+ pointer to a struct of this type, simply pass it `myStruct.pointer`.
+ Whether this member is of type Number or BigInt depends on whether
+ the WASM environment is 32-bit (Number) or 64-bit (BigInt).
+
+- `ptrAdd(args...)`
+ Equivalent to [StructBinder][]`.ptrAdd(this.pointer, args...)`
+ or [StructCtor][]`.ptrAdd(this.pointer, args...)`.
+
- `setMemberCString(memberName,str)`
Uses `StructType.allocCString()` to allocate a new C-style string,
assign it to the given member, and add the new string to this
from JS be kept to a minimum or that the relationship be one-way:
let C manage the strings and only fetch them from JS using, e.g.,
`memberToJsString()`.
-
-<a name='api-structctor'></a>
+- `zeroOnDispose` (bool, read-only)
+ True if this instance or its prototype were configured with
+ the `zeroOnDispose` flag.
+
+
+<a id='api-structctor'></a>
API: Struct Constructors
------------------------------------------------------------
```
Normally they should be passed no arguments, but they optionally
-accept a single argument: a WASM heap pointer address of memory
-which the object will use for storage. It does _not_ take over
-ownership of that memory and that memory must be valid at
-for least as long as this struct instance. This is used, for example,
-to proxy static/shared C-side instances:
+accept a single argument: a WASM heap pointer address of memory which
+the object will use for storage. It does _not_ take over ownership of
+that memory and that memory must remain valid for at least as long as
+this struct instance. This is used, for example, to proxy
+static/shared C-side instances or those which we simply want to
+get access to via this higher-level API for a while:
>
```
const x = new MyStruct( someCFuncWhichReturnsAMyStructPointer() );
...
-x.dispose(); // does NOT free the memory
+x.dispose(); // does NOT free or zero the memory
```
The JS-side construct does not own the memory in that case and has no
way of knowing when the C-side struct is destroyed. Results are
specifically undefined if the JS-side struct is used after the C-side
-struct's member is freed.
+struct's member is freed. However, if the client has allocated that
+memory themselves and wants to transfer ownership of it to the
+instance, that can be done with:
+
+>
+```
+x.addOnDispose( thePtr );
+```
+
+Which will free `thePtr` when `x.dispose()` is called.
+
+As of 2025-11-10, a third option is available:
-> Potential TODO: add a way of passing ownership of the C-side struct
-to the JS-side object. e.g. maybe simply pass `true` as the second
-argument to tell the constructor to take over ownership. Currently the
-pointer can be taken over using something like
-`myStruct.ondispose=[myStruct.pointer]` immediately after creation.
+>
+```
+const x = new MyStruct({
+ wrap: ptr, // as per MyStruct(ptr)
+ takeOwnership: bool, // If true, take ownership of the wrap ptr.
+ zeroOnDispose: bool , // if true, overrides MyStruct.structInfo.zeroOnDispose
+ extraBytes: int // bytes to alloc after the end of the struct
+});
+```
+
+- If `wrap` is set then (A) it must be at least
+ `MyStruct.structInfo.sizeof` of memory owned by the caller, (B) it
+ _must_ have been allocated using the same allocator as Jaccwabyt is
+ configured for, and (C) ownership of it is transfered to the new
+ object. `zeroOnDispose` and `extraBytes` are disregarded if `wrap`
+ is set. A falsy `wrap` value is equivalent to not providing one and
+ a non-integer value, or a number less than 0, triggers an error.
+
+- If `takeOwnership` is truthy then this object takes over ownership
+ of the `wrap` pointer (if any). This flag is otherwise ignored.
+
+- If `zeroOnDispose` or `extraBytes` are are used without `wrap`, each
+ which is used is set as a read-only propperty on the resulting
+ `MyStruct` object, except that `zeroOnDispose` is only recorded if
+ it is truthy and `extraBytes` is only recorded if it is not 0 (a
+ negative value, or non-integer, triggers an exception).
+
+- `ondispose`: if set it is passed to the new object's
+ `addOnDispose()` before the constructor returns, so may have any
+ value legal for that method. Results are undefined if `ondispose`
+ and `wrap` have the same pointer value (because `wrap` will already
+ be cleaned up via `dispose()`, as if it had been passed to
+ `addOnDispose()`).
+
+In the case of `extraBytes`, a pointer to the tail of the memory can
+be obtained by adding `theInstance.pointer` to
+`theInstance.structInfo.sizeof`, with the caveat that `pointer` may be
+a BigInt value (on 64-bit WASM), `sizeof` will be a Number and,
+spoiler alert, `(BigInt(1) + Number(1))` is not legal. Thus this API
+adds a small convenience method to work around that portability issue:
+
+>
+```
+const ptrTail = MyStruct.ptrAdd(theInstance.pointer, theInstance.extraBytes);
+```
+
+`typeof ptrTail` is `'bigint'` in 64-bit builds and `'number'` in
+32-bit. i.e. it's the same type as `theInstance.pointer`.
+Equivalently, the inherited `MyStruct` instance method with the same
+name adds an instance's own `pointer` value to its arguments:
+
+```
+const ptrTail = theInstance.ptrAdd(theInstance.extraBytes);
+```
These constructors have the following "static" members:
- `memberKeys(string)`
Works exactly as documented for [StructType][].
+- `ptrAdd(args...)`
+ Equivalent to [StructBinder][]`.ptrAdd(args...)`. The [_inherited_
+ method with the same name][StructType] behaves differently.
+
- `structInfo`
The structure description passed to [StructBinder][] when this
constructor was generated.
- `structName`
The structure name passed to [StructBinder][] when this constructor
was generated.
-
-<a name='api-structprototype'></a>
+
+<a id='api-structprototype'></a>
API: Struct Prototypes
------------------------------------------------------------
The prototypes of structs created via [the constructors described in
-the previous section][StructCtors] are each a struct-type-specific
+the previous section][StructCtor] are each a struct-type-specific
instance of [StructType][] and add the following struct-type-specific
properties to the mix:
The name of the struct, as it was given to the [StructBinder][] which
created this class.
-<a name='api-structinstance'></a>
+<a id='api-structinstance'></a>
API: Struct Instances
------------------------------------------------------------------------
Instances of structs created via [the constructors described
-above][StructCtors] each have the following instance-specific state in
-common:
-
-- `pointer`
- A read-only numeric property which is the "pointer" returned by the
- configured allocator when this object is constructed. After
- `dispose()` (inherited from [StructType][]) is called, this property
- has the `undefined` value. When calling C-side code which takes a
- pointer to a struct of this type, simply pass it `myStruct.pointer`.
+above][StructCtor]. Each inherits all of the methods and properties
+from their constructor's prototype.
-<a name='appendices'></a>
+<a id='appendices'></a>
Appendices
============================================================
-<a name='appendix-a'></a>
+<a id='appendix-a'></a>
Appendix A: Limitations, TODOs, and Non-TODOs
------------------------------------------------------------
- This library only supports the basic set of member types supported
- by WASM: numbers (which includes pointers). Nested structs are not
- handled except that a member may be a _pointer_ to such a
- struct. Whether or not it ever will depends entirely on whether its
- developer ever needs that support. Conversion of strings between
- JS and C requires infrastructure specific to each WASM environment
- and is not directly supported by this library.
-
-- Binding functions to struct instances, such that C can see and call
- JS-defined functions, is not as transparent as it really could be,
- due to [shortcomings in the Emscripten
- `addFunction()`/`removeFunction()`
- interfaces](https://github.com/emscripten-core/emscripten/issues/17323). Until
- a replacement for that API can be written, this support will be
- quite limited. It _is_ possible to bind a JS-defined function to a
- C-side function pointer and call that function from C. What's
- missing is easier-to-use/more transparent support for doing so.
- - In the meantime, a [standalone
- subproject](/file/common/whwasmutil.js) of Jaccwabyt provides such a
- binding mechanism, but integrating it directly with Jaccwabyt would
- not only more than double its size but somehow feels inappropriate, so
- experimentation is in order for how to offer that capability via
- completely optional [StructBinderFactory][] config options.
-
-- It "might be interesting" to move access of the C-bound members into
- a sub-object. e.g., from JS they might be accessed via
- `myStructInstance.s.structMember`. The main advantage is that it would
- eliminate any potential confusion about which members are part of
- the C struct and which exist purely in JS. "The problem" with that
- is that it requires internally mapping the `s` member back to the
- object which contains it, which makes the whole thing more costly
- and adds one more moving part which can break. Even so, it's
- something to try out one rainy day. Maybe even make it optional and
- make the `s` name configurable via the [StructBinderFactory][]
- options. (Over-engineering is an arguably bad habit of mine.)
-
-- It "might be interesting" to offer (de)serialization support. It
- would be very limited, e.g. we can't serialize arbitrary pointers in
- any meaningful way, but "might" be useful for structs which contain
- only numeric or C-string state. As it is, it's easy enough for
- client code to write wrappers for that and handle the members in
- ways appropriate to their apps. Any impl provided in this library
- would have the shortcoming that it may inadvertently serialize
- pointers (since they're just integers), resulting in potential chaos
- after deserialization. Perhaps the struct description can be
- extended to tag specific members as serializable and how to
- serialize them.
-
-<a name='appendix-d'></a>
+ by WASM: numbers (which includes pointers).
+
+- Binding JS functions to struct instances, such that C can see and
+ call JS-defined functions, is not as transparent as it really could
+ be. [The WhWasmUtil API][whwasmutil.js], and
+ standalone subproject co-developed with Jaccwabyt, provides such a
+ binding mechanism. There is some overlap between the two APIs and
+ they arguably belong bundled together, but doing so would more than
+ triple Jaccwabyt's size. (That said, the only known Jaccwabyt
+ deployment uses both APIs, so maybe it's time to merge them (he says
+ on 2025-11-10). As of this writing, jaccwabyt.js is 38k, half of
+ which is comments/docs, whereas whwasmutil.js is 100kb and 75%
+ docs).
+
+<a id='appendix-b'></a>
+Appendix B: Build
+------------------------------------------------------------------------
+
+In order to support both vanilla JS and ESM (ES6 module) builds from a
+single source, this project uses [a preprocessor][c-pp], which requires
+only a C compiler:
+
+> $ make
+
+The makefile requires GNU make, not BSD or POSIX make.
+
+The resulting files are in the `js/` subdirectory, in both "vanilla"
+JS and ESM formats.
+
+This tree [includes all of the requisite sources](/dir/tool) and
+requires no out-of-tree dependencies beyond the system's libc.
+
+
+<a id='appendix-d'></a>
Appendix D: Debug Info
------------------------------------------------------------
[StructType][].
-<a name='appendix-g'></a>
+<a id='appendix-g'></a>
Appendix G: Generating Struct Descriptions From C
------------------------------------------------------------
div.content h3 {border-left-width: 2.5em}
</style>
-[sqlite3]: https://sqlite.org
-[emscripten]: https://emscripten.org
-[sgb]: https://wanderinghorse.net/home/stephan/
[appendix-g]: #appendix-g
-[StructBinderFactory]: #api-binderfactory
-[StructCtors]: #api-structctor
-[StructType]: #api-structtype
+[BigInt64Array]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt64Array
+[c-pp]: https://fossil.wanderinghorse.net/r/c-pp
+[Emscripten]: https://emscripten.org
+[jaccwabyt.js]: /file/jaccwabyt/jaccwabyt.c-pp.js
+[MDN]: https://developer.mozilla.org/docs/Web/API
+[sgb]: https://wanderinghorse.net/home/stephan/
+[sqlite3]: https://sqlite.org
[StructBinder]: #api-structbinder
+[StructBinderFactory]: #api-binderfactory
+[StructCtor]: #api-structctor
[StructInstance]: #api-structinstance
-[^export-func]: In Emscripten, add its name, prefixed with `_`, to the
- project's `EXPORT_FUNCTIONS` list.
-[BigInt64Array]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt64Array
+[StructType]: #api-structtype
[TextDecoder]: https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder
[TextEncoder]: https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder
-[MDN]: https://developer.mozilla.org/docs/Web/API
+[WASI-SDK]: https://github.com/WebAssembly/wasi-sdk
+[whwasmutil.js]: /file/wasmutil/whwasmutil.c-pp.js
+
+[^export-func]: In Emscripten, add its name, prefixed with `_`, to the
+ project's `EXPORT_FUNCTIONS` list.