Skip to content

tsbindgen v1: C headers to tslang bindings - #367

Merged
ASDAlexander77 merged 9 commits into
mainfrom
tsbindgen-v1
Sep 26, 2026
Merged

ASDAlexander77 merged 9 commits into
mainfrom
tsbindgen-v1

Conversation

@ASDAlexander77

@ASDAlexander77 ASDAlexander77 commented Sep 26, 2026 •

Copy link
Copy Markdown
Owner

Draft, stacked on #366 (linkname-decorator). Retarget to main once that merges. CI for pull requests runs only against main, so both workflows were started by hand on this branch; the Linux result is below once it's in.

PR 2 of docs/superpowers/specs/2026-09-25-tsbindgen-linkname-design.md. It adds tsbindgen, a tool that reads a C header with clang and writes tslang bindings for it.

tsbindgen fixture.h [-o fixture.ts] [--namespace N [--strip-prefix P]] [--filter GLOB] -- <clang args>

What it writes

  • Functions: declare function, with @linkname("c_name") whenever the TS name differs from the symbol. That covers --namespace, --strip-prefix, escaped reserved words and __asm__ labels.
  • Types:
    • structs become named tuples;
    • enums become enums of the C integer type;
    • typedefs become type aliases;
    • literal object-like macros become consts.
  • Pointers: Reference<T>, or Opaque for handles, void *, function pointers and C++ classes. char * is string.
  • Integers: signed map to sN and unsigned to uN, by the width the target gives them.
  • Skips: anything it can't bind correctly is skipped with a // skipped: <name>: <reason> line and a warning, never dropped silently. Examples:
    • variadic functions;
    • bit-fields;
    • packed or over-aligned layouts;
    • unions;
    • non-C calling conventions;
    • function-like and non-literal macros.

The code is in three places:

  • tslang/lib/TsClangImporter: the binding model, clang HeaderParser, TypeMapper and BindingPrinter.
  • tslang/tsbindgen: the command line.
  • tslang/unittests/TsClangImporter: 53 gtest cases.

Where it differs from the spec

The full table is in the spec's new "As built (2026-09-26)" section. Each row was found by running generated bindings through tslang:

  • The output is .ts, not .d.ts. A const in a .d.ts, or in any file pulled in with import, has no value: LNK2019 when linked, Symbols not found under the JIT. The bindings are included with /// <reference path>.
  • Signed integers are sN, not iN. tslang's iN are signless, so an int8_t -5 printed 251.
  • A pointer field that leads back to its own struct is Opaque. A self-referencing type alias makes tslang exit 127 with no message. That's a compiler bug to file separately.
  • A function-pointer signature is an inline /* ... */ comment on the parameter. The spec had one trailing comment, which can't say which parameter it describes.
  • clang runs through ToolInvocation with our own diagnostics. runToolOnCodeWithArgs exits 0 even when the clang command line has an error.
  • Four behaviors the spec left open:
    • non-literal macros are skipped with a reason;
    • --strip-prefix never produces an empty, digit-leading or duplicate name;
    • structs with an unnatural layout are skipped;
    • "nothing selected" is a warning.

tslang/docs/reference-counting-evaluation.md §9.78 records the open issue: under -mm=rc, a C-owned char * returned as string has no string header. v1 targets gc.

Tests

  • unittest-TsClangImporterTests (53 cases):
    • type mapping on x86_64 Linux, x86_64 Windows and i686;
    • naming and escaping;
    • selection;
    • every skip reason;
    • printer output.
  • test-bindgen-plain and test-bindgen-namespace: build fixture.c into a real C library at -O2, generate bindings from fixture.h, and run bindgen_test.ts both linked and under --emit=jit. The output must match expected.txt.
    • The narrow row passes int8_t/uint16_t/bool arguments. It exists to catch a missing signext/zeroext on tslang's declare parameters. That can only fail on SysV (Linux), where an optimized callee trusts the caller to extend.
    • If it fails there, it's a tslang call-lowering bug to file, not a mapping change.
  • test-bindgen-cli: pins the exit codes and messages for bad arguments, a missing header and a clang error.
  • Full Release suite on Windows: all pass, with one test disabled; that one belongs to @linkname: a second name for @dllname, and one C symbol for many declarations #366.

Checked against the default library

Bindings generated from DefaultLib's regex.cpp and http.cpp compile in tslang. Compared with the hand-written lib.native.d.ts, the 22 functions in each have the same names. The only differences are expected:

  • Opaque versus a named or commented Opaque handle;
  • int versus s32;
  • one parameter named s by hand where the C++ source says c;
  • an omitted : void.

No mistakes were found in lib.native.d.ts. The comparison did find one generator bug: std::cmatch * came out as an undeclared match_results. It's fixed in 6918335 with a unit test.

Known gaps (v1)

Nothing is emitted, and no skip line is written, for these:

  • extern global variables;
  • anonymous enum { FOO = 1 } constants;
  • typedef struct {…} *Handle (the typedef and its users).

Smaller:

  • enum member names aren't checked against reserved words;
  • a non-static C99 inline function is emitted as a declaration;
  • float macros print at full precision.

Linux CI

Run 36246544195 on b3a03ca3: 2897/2899 pass. test-bindgen-plain and test-bindgen-namespace fail on exactly one line, the narrow row, both linked and under the JIT. Windows is green.

expected: narrow -5 65535 -5 false true
got:      narrow 251 65535 -5 false true

fx_widen_s8(-5) returns 251. On x86-64 SysV, a clang-compiled callee assumes the caller has already sign- or zero-extended an 8- or 16-bit argument to 32 bits. tslang never emits signext/zeroext; nothing in lib/TypeScript sets either. So tslang passes the raw 8 bits and the callee reads garbage in the upper bits. u16 passed only because the 16-bit constant happened to arrive zero-extended.

This is the tslang call-lowering bug from Review Focus 1, not a mapping error. It affects every tslang call into C with an 8- or 16-bit argument on Linux, hand-written bindings included. The mapping stays as it is.

Fixed in #369. This branch with #369 cherry-picked passes on Linux: 2907/2907 (run 36250596356). #367 goes green once it's rebased onto main after #366 and #369 merge.

(An earlier run failed to build: gcc -Wpedantic rejects &main. Fixed in b3a03ca.)

🤖 Generated with Claude Code

ASDAlexander77 and others added 9 commits September 26, 2026 18:37
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed C integers map to sN (tslang's iN are signless), and a struct
pointer that leads back to its own struct is Opaque: tslang cannot
declare a self-referencing type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…paque

Found by running tsbindgen over the default library's regex wrappers:
their functions take a std::cmatch *, which was emitted as a type name
(match_results) nothing declares, so the bindings did not compile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the branch review:
- a C type named like a tslang type (typedef int boolean) is looked
  through, a struct of such a name renamed: tslang ignores such an alias
  and bound an int result as boolean's one bit
- a C name TS reserves (int new(void)) gets a `_` and @Linkname
- an __asm__ label is the symbol (glibc's __REDIRECT)
- a malformed number in an unexpanded macro skips that macro instead of
  failing the header
- a non-C calling convention (__stdcall on i686) is skipped, not bound
  as cdecl
- an enum declared but never defined is its integer type
- the e2e fixture is built at -O2, where a callee relies on its caller
  extending narrow arguments

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gcc -Wpedantic -Werror (Linux CI) rejects &main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 marked this pull request as ready for review September 26, 2026 18:19
@ASDAlexander77
ASDAlexander77 merged commit 22878dd into main Sep 26, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the tsbindgen-v1 branch September 26, 2026 18:19
ASDAlexander77 added a commit that referenced this pull request Sep 26, 2026
…372)

tsbindgen has built alongside tslang since #367, but the release
packaging step only ever staged an explicit allowlist of binaries, so
it never reached the zip/tarball. It also needs clang's resource
directory (stddef.h, stdint.h, ...) next to it, or it fails at runtime
with "clang's resource directory not found" - findResourceDir's
release-package candidate is exeDir/lib/clang/<major>, so stage
3rdParty/llvm's lib/clang folder at that path in both packages.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
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