Repository navigation
@cppvtable classes: call C++ virtual methods through the object's own vtable - #550
Merged
Merged
Conversation
… 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This adds a
@cppvtableclass 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'snvrhi::IDevice, can then be used from TypeScript without a flatextern "C"wrapper per method:A tslang class already matched a C++ single-inheritance object:
fn(this, args), which is the MSVC x64 member-function convention;What didn't match is what tslang adds to the vtable:
.instanceOfin slot 0,.new, interface slots, and the.rtti/.sizestatic-field slots, which land between base and derived methods. A@cppvtableclass 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:
implementsextendsbetween a@cppvtableclass and an ordinary class, in either directioninstanceof, a cast to or fromany, a cast to an interface-mm=rcand-mm=own__thiscallthere. This is a newTargetInfo::memberCallsUseThiscallpredicateDeclarationPrinterkeeps@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
nvrhi::RefCountPtr<T>): MSVC returns these through a hidden pointer afterthis. For now that pointer is written as an explicit parameter. Lowering it in the compiler would be a follow-up.Tests
tests/00cppvtable_class.ts: checks slots, fields,extendsandabstractagainst 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-sharedAOT and JIT.cppvtable/*.ts: one compile error per rule, with checked message and position. Also an rc/none pair and an i686__thiscalltest.TSLANG_CORPUS, because that corpus also runs under rc.Checked against real NVRHI
In native_ts_graphics,
NvResource/NvDevice/NvCommandList/NvBufferwere bound as@cppvtableclasses, with slot numbers fromcl /d1reportSingleClassLayout. A JIT run did the following, each step called directly from TypeScript: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