Skip to content

@cppvtable classes: call C++ virtual methods through the object's own vtable - #550

Merged
ASDAlexander77 merged 1 commit into
mainfrom
cppvtable-class-decorator
Oct 11, 2026
Merged

ASDAlexander77 merged 1 commit into
mainfrom
cppvtable-class-decorator

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

Summary

This adds a @cppvtable class decorator. A class marked with it is laid over objects that C++ made, and its methods are called through the C++ object's own vtable. Single-inheritance C++ interfaces, such as NVRHI's nvrhi::IDevice, can then be used from TypeScript without a flat extern "C" wrapper per method:

@cppvtable
class NvResource {
    _destructor(): void;  // slot 0
    AddRef(): u32;        // slot 1
    Release(): u32;       // slot 2
    GetRefCount(): u32;   // slot 3
}

@cppvtable
class NvDevice extends NvResource {
    // ...
    getGraphicsAPI(): u8; // slot 39
}

const device = <NvDevice>Donut_GetNvrhiDevice(app);
print(device.getGraphicsAPI());

A tslang class already matched a C++ single-inheritance object:

  • the vtable pointer is at offset 0;
  • slot N is called as fn(this, args), which is the MSVC x64 member-function convention;
  • base slots come first.

What didn't match is what tslang adds to the vtable: .instanceOf in slot 0, .new, interface slots, and the .rtti/.size static-field slots, which land between base and derived methods. A @cppvtable class has none of these. Its methods are the slots from 0 in declaration order, base first. It also gets no RTTI, vtable global, constructor or allocation of its own.

Errors

A slot off by one calls a different C++ method with no error, so everything that would shift slots, or ask the object what it is, is a compile error:

Not allowed Why
Method bodies, constructors, field initializers The objects and their code belong to C++
Static members, accessors They would take vtable slots
implements An implemented interface takes a slot
extends between a @cppvtable class and an ordinary class, in either direction The ordinary class's slots would shift the numbering
instanceof, a cast to or from any, a cast to an interface Each one asks the object through slot 0, which is a C++ method
-mm=rc and -mm=own They write a reference count in the word before the object, which is inside the C++ object
32-bit x86 MSVC targets MSVC member functions are __thiscall there. This is a new TargetInfo::memberCallsUseThiscall predicate

DeclarationPrinter keeps @cppvtable, so an exported class re-imports with the same slots. Without it, the importer would number them as an ordinary class.

Not in this PR

  • Methods that return a C++ class by value (e.g. nvrhi::RefCountPtr<T>): MSVC returns these through a hidden pointer after this. For now that pointer is written as an explicit parameter. Lowering it in the compiler would be a follow-up.
  • C++ overloads: these still need distinct TypeScript names, because methods are identified by name.

Tests

  • tests/00cppvtable_class.ts: checks slots, fields, extends and abstract against a vtable built by hand. Runs AOT and JIT.
  • tests/export_cppvtable_class.ts + import_cppvtable_class.ts: the same slots across modules, with the exporting module calling through its own numbering. Runs -shared AOT and JIT.
  • cppvtable/*.ts: one compile error per rule, with checked message and position. Also an rc/none pair and an i686 __thiscall test.
  • Excluded from rc-only runs: the ownership verifier's rc sweep and the i686 twins skip the positive files, with the reason recorded. They aren't in TSLANG_CORPUS, because that corpus also runs under rc.
  • Full suite: 3996/3996 pass on Release (VS 2026).

Checked against real NVRHI

In native_ts_graphics, NvResource/NvDevice/NvCommandList/NvBuffer were bound as @cppvtable classes, with slot numbers from cl /d1reportSingleClassLayout. A JIT run did the following, each step called directly from TypeScript:

  1. created a command list and two buffers;
  2. uploaded 64 values;
  3. copied them to a readback buffer;
  4. mapped it and compared: 0 mismatches;
  5. released everything through a NvResource[].

It passed on DX12 and Vulkan, with and without the debug and validation layers.

Docs: docs/cppvtable-classes.md. docs/c-cpp-header-import.md §4 now links to it.

🤖 Generated with Claude Code

… vtable

A class decorated with @cppvtable is laid over objects made by C++. Its methods
are the C++ vtable's slots from 0 in declaration order, with a base class's
slots first. The class gets no .instanceOf, .new, interface or static-field
slots, no RTTI, and no vtable, constructor or allocation of its own. A tslang
class already keeps its vtable pointer at offset 0 and calls slot N as
fn(this, args), which is the MSVC x64 member-function convention, so this is
all that was missing to bind single-inheritance C++ interfaces such as
nvrhi::IDevice.

A slot off by one calls a different C++ method with no error, so these are
compile errors:
- method bodies, constructors, field initializers;
- static members, accessors;
- implements, and mixing with ordinary classes through extends;
- instanceof, and casts to or from any or an interface (each asks the object
  through slot 0, which is a C++ method);
- rc/own memory models (they write a count before the object);
- 32-bit x86 MSVC targets (__thiscall).

The declaration printer keeps @cppvtable, so an exported class re-imports with
the same slots.

Tests:
- tests/00cppvtable_class.ts: slots, fields, extends and abstract, against a
  vtable made by hand (AOT and JIT);
- export/import_cppvtable_class.ts: the same slots across modules;
- cppvtable/*.ts: one compile error per rule.

Docs: docs/cppvtable-classes.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit 67cbaee into main Oct 11, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the cppvtable-class-decorator branch October 11, 2026 17:19
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