p.enthalabs

ts-polyglot — C, C++ and Rust functions in TypeScript

early MIT Windows · Linux · macOS Node 22.18+TS 7.1 beta

Call C, C++ and Rust functions from TypeScript through Node-API, with no bindings or `.d.ts` to write. Types come straight from the native signatures, structs included, and the addon built on first import enforces the same types at run time.

`npm create ts-polyglot`

How it works

native source

`#include <math.h>struct Point { double x; double y; };double distance(struct Point a, struct Point b) { return hypot(a.x - b.x, a.y - b.y);}` main.ts imports geometry.c

`import { distance, type Point } from "./geometry.c";// interface Point { x: number; y: number }, from the C structconst p: Point = { x: 3, y: 4 };distance({ x: 0, y: 0 }, p); // 5`

Demo

Import from a C++ file, get completions typed from its signature, then do the same with Rust.

Set breakpoints in `main.ts` and `numbers.rs` and step from TypeScript into the Rust function and back.

A C struct becomes `interface Point` with typed fields. The debugger stops inside `distance`, and after the fields are renamed in C, tsc flags the old names in `main.ts`.

Video 3

0:00 / 0:00

How it works

One source file, two consumers. Both read its signatures with the same tree-sitter parser and the same type mapping, so the types `tsc` checks are the ones the addon enforces.

At type-check time

1. geometry.c ``` struct Point { double x; double y; }; double distance(struct Point a, struct Point b) ```

2. tree-sitter parses the file and finds each struct and function; nothing is compiled

3. type marshaling maps each native type to TypeScript: `double` → `number`, a struct → an `interface`

4. tsc and your editor ``` interface Point { x: number; y: number } distance(a: Point, b: Point): number ```

TypeScript gets the types from a content mapper, a TypeScript 7.1 beta feature. Every native type that crosses is in the type mapping; protocol and editor notes are in docs/content-mappers.md.

At run time

1. main.ts`distance({ x: 0, y: 0 }, p)`

2. first import geometry.c and its Node-API glue are compiled to an addon

3. type marshaling on every call, the glue reads each object into a struct Point; a missing field is a TypeError

4. native call`5`

Node gets the functions from a Node-API addon that ts-polyglot compiles the first time the file is imported and caches by source hash. After that, each call goes through the glue’s checks straight into native code.

Type mapping

What crosses between TypeScript and each language in v0.3.0. C++ takes every C spelling too, so its column lists only what it adds. Every detail is in the package README’s type mapping.

Types

TypeScript types and the native spellings that map to them | TypeScript | C | C++ | Rust | | --- | --- | --- | --- | | `boolean` | `bool`, `_Bool` | `bool` | | `number`integers up to 32 bits | `char`, `short`, `int`, `(u)int8_t`, `(u)int16_t`, `(u)int32_t`signed or unsigned | `i8`, `i16`, `i32`, `u8`, `u16`, `u32` |

| `bigint`64-bit integers; a parameter also takes a number | `long`, `long long`, `(u)int64_t`, `size_t`, `ptrdiff_t`, `(u)intptr_t`signed or unsigned; long is 32 bits on Windows, and the glue checks the real width | `i64`, `u64`, `isize`, `usize` | | `number`floating point | `float`, `double`a float is narrowed | `f32`, `f64` | | `string`one character | a C char is a number | `char`one Unicode scalar value | | `string | null` | `const char*`a returned pointer is copied and never freed; NULL is null | none |

| `string`UTF-8 on the native side | `char name[N]`, `const char s[]`sized: at most N − 1 bytes; unsized: takes a length parameter | `std::string`, `const std::string&`, `std::string_view` | `&str`, `String`, `&String`, `&'static str`&'static str as a result | | `interface`named after the struct, every field required | `struct`only data members, defined in the same file | `class`, `const T&`only public data members | `pub struct`, `&T`only pub fields, same file; no #[repr(C)] needed | | `readonly T[]`T[] as a result or field | `const T xs[]`the next integer parameter is its length, filled in by the glue | `std::vector<T>`, `const std::vector<T>&` | `Vec<T>`, `&[T]`, `&Vec<T>`, `&'static [T]`&'static [T] as a result | | `readonly [T, T, T]`a tuple of N; not readonly as a result or field | `T x[N]`, `const T x[N]`a field, a parameter | `std::array<T, N>`, `const std::array<T, N>&` | `[T; N]`, `&[T; N]` | | `Uint8Array`copied both ways | `uint8_t`, `unsigned char`as the element of any array or vector | `u8`as the element of any array, slice or Vec | | `void`results only | `void` | `()`or no return type |

Functions

Which functions each language exports | | C | C++ | Rust | | --- | --- | --- | | Exported | function definitions that aren’t static, at the top level | `extern "C" { }`, `namespace`inside these too, under the function’s own name | `pub fn`, `pub extern "C" fn`, `pub const fn`at the top level or inside a pub mod |

| Skipped for now importing one is a “has no exported member” error | `T*`, `enum`, `union`pointers other than const char*, non-const arrays, function pointers, variadics, an unknown macro before the return type | `T&`, `template`, `std::optional`, `std::map`mutable references, overloads past the first, class methods | `&mut T`, `Option<T>`, `Box<T>`, `enum`, `impl`generics, tuples, i128 and u128, unsafe and async functions |

Checks on every call

| `TypeError` | The wrong JavaScript type: `"1"` for an `int`, `null` for a struct, a missing field, a `number[]` for a `Uint8Array`. | | --- | | `RangeError` | The right type, an impossible value: `1.5` or `2 ** 40` for an `int`, `-1` for an unsigned, 2 elements for a `std::array<int, 3>`. The message gives the limit. | | `Error` | The native code failed: a C++ exception (its `what()`) or a Rust panic (its message). |

Arguments are checked before any native code runs, and a rejected value is named by its path: `area(): p.pts[0].y must be a number`.

Rules

- Everything crosses by value: arguments are copies, results are fresh JavaScript values, and nothing is written back.

- Structs and arrays nest to any depth: `Vec<Vec<i32>>` is `number[][]`.

- An integer takes a whole `number` inside the type’s limits and under 2 53 in size, or a `bigint`. A `bool` takes only `true` or `false`.

- Struct definitions and array sizes come from the same file: `#include`, `mod` and `use` aren’t followed. A size is a literal or a name the file defines (`#define`, `const`, `constexpr`).

Start a project

`npm create ts-polyglot`

Skip the questions

`npm create ts-polyglot my-app -- --ide none --no-install`

The scaffolder

1. Asks for a directory and an editor. 2. Writes a project with a C, a C++ and a Rust file imported from `main.ts`, plus the editor setup and F5 debugging. 3. Installs the dependencies. 4. Checks for the compilers. 5. Prints what’s left. For VS Code: install the recommended TypeScript 7 extension and pick **Use Workspace Version**.

Requirements

- **Windows** (x64), **Linux** or **macOS**

- Node **22.18+**

- Rust, for `.rs` files

- A C and C++ compiler for your platform

The Visual Studio 2022 C++ build tools, with the **Desktop development with C++** workload.

Working on the repo

| Path | What | | --- | --- | | packages/ts-polyglot | The package: content mapper and runtime hook | | packages/create-ts-polyglot | `npm create ts-polyglot`: new project with demo files, editor setup and F5 debugging | | examples/native-demo | C, C++ and Rust files imported from `main.ts` | | docs/content-mappers.md | Notes on the content mapper protocol and editor support |

Try it from a clone

npm installnpm run typechecknpm run example