Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/c-cpp-header-import.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,11 @@ C is a 90%-mechanical translation; C++ is not. Realistic phasing:
| C++ 1 | Free functions, non-template classes with methods (no virtuals crossing the boundary), overloads | Bind by **mangled name**: clang provides Itanium/MSVC mangled symbols (`clang::MangleContext`); emit `declare` per overload with the mangled name attached (`@linkname("?foo@@YAHH@Z")`, an alias of `@dllname`, makes the emitted `FuncOp` symbol differ from the TS-visible name). Methods become functions taking `this: Reference<S>` first. Constructors/destructors bound explicitly; caller manages object memory (`GC_malloc_atomic` for PODs / explicit new-delete pair bindings). | 2–4 weeks after A |
| C++ 2 | Templates, virtual dispatch, exceptions, STL types | **Do not bind directly.** Generate a flattened `extern "C"` wrapper `.cpp` (tsbindgen emits both the wrapper and its `.d.ts`), compiled with the system C++ compiler and linked via existing `-obj=`. Explicit template instantiations only; exceptions caught at the wrapper boundary and converted to error codes (C++ exceptions must never unwind into tslang frames — different personality functions, especially in JIT where Win64 unwind info is custom, see `docs/jit-gc-static-roots.md` context). | 3–6 weeks, per-need |

Virtual dispatch has since become bindable without a wrapper for single-inheritance
interfaces: a `@cppvtable` class calls a C++ object's methods through its own vtable (see
[cppvtable-classes.md](cppvtable-classes.md)). Templates, exceptions and STL types still need
the wrapper route.

The C++ ABI reality (mangling differences, MSVC vs Itanium, RTTI, std::string
layout) is why every successful system (Swift, Rust `cxx` crate, Zig) either uses
clang as the importer *and* restricts the surface, or generates C wrappers. The
Expand Down
95 changes: 95 additions & 0 deletions docs/cppvtable-classes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# `@cppvtable` classes — calling C++ virtual methods directly

A class decorated with `@cppvtable` is laid over objects made by C++. tslang calls the methods
through the C++ object's own vtable, so a C++ interface such as NVRHI's `nvrhi::IDevice` can be
used from TypeScript without a flat `extern "C"` wrapper per method:

```ts
@cppvtable
class NvResource {
_destructor(): void; // slot 0: the C++ virtual destructor, declared only to keep the count
AddRef(): u32; // slot 1
Release(): u32; // slot 2
GetRefCount(): u32; // slot 3
}

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

const device = <NvDevice>Donut_GetNvrhiDevice(app); // an Opaque from C/C++
print(device.getGraphicsAPI());
```

## How it maps

A tslang class already matches a C++ single-inheritance object on 64-bit targets in the
following ways:

- the vtable pointer is the object's first word;
- a method call loads slot N of that table and calls it as `fn(this, args...)`, with no hidden
parameters, in the target's C calling convention (on x64 Windows that is the MSVC member
function convention);
- a base class's slots come first, and a base object is the prefix of a derived one.

What `@cppvtable` changes is what the class adds to the vtable. An ordinary class has
`.instanceOf` in slot 0, `.new`, a slot for each implemented interface, and slots for its static
fields (`.rtti`, `.size`). A `@cppvtable` class has none of these. Its methods are the vtable's
slots from 0, in declaration order, with a base class's slots first.

The class gets no vtable, constructor, RTTI or allocation of its own. It is a declaration: its
methods have no bodies, and an `abstract` class with `abstract` methods reads the same.

Fields declared on the class are the C++ object's fields after the vtable pointer, laid out like
a tuple's.

## Rules

The compiler rejects anything that would add a slot, run code of its own, or ask the object what
it is. A slot number off by one calls a different C++ method with no error at all, which is why
these are errors and not warnings.

| Not allowed | Why |
| --- | --- |
| a method with a body, a constructor, a field initializer | the objects and their code are C++'s |
| static members, `get`/`set` accessors, `accessor` fields | they would take vtable slots, and accessor slots don't follow declaration order |
| `implements` | an implemented interface takes a slot |
| `extends` an ordinary class, or an ordinary class extending a `@cppvtable` one | the ordinary class's slots (`.instanceOf`, `.rtti`, ...) would shift the numbering |
| `instanceof`, a cast to or from `any`, a cast to an interface | each asks the object through slot 0, which is a C++ method |
| `-mm=rc` / `-mm=own` | retains and releases write a count in the word before an object, inside the C++ object |
| a 32-bit x86 MSVC target | C++ member functions are `__thiscall` there (`this` in ECX) |

A generic or static class can't be `@cppvtable` either.

## Writing a binding

- **Slot numbers.** Take them from the compiler, not by counting: MSVC prints a class's vtable
with `cl /c /Zs /d1reportSingleClassLayout<Name> file.cpp`. Declare unused slots with
placeholder names (`_slot17(): void;`) so the methods you call keep their numbers.
- **The destructor** is one slot under MSVC (the scalar deleting destructor) and two under the
Itanium ABI. Don't call it: release objects the way the C++ library does (`Release()`).
- **Overloads.** tslang methods are identified by name, so every C++ overload needs its own
TypeScript name. MSVC groups overloads in the vtable, and not always in declaration order:
check the slot numbers in the layout report.
- **Struct returned by value.** A C++ method that returns a class or struct by value (for
example `nvrhi::RefCountPtr<T>`) returns it through a hidden pointer. Under MSVC that pointer
is passed after `this`, and the method returns it. Declare the method with that parameter:

```ts
createBuffer(ret: Ref<Opaque | null>, desc: Ref<BufferDesc>): Opaque; // RefCountPtr<IBuffer>
```

The pointer it writes holds a reference, which the caller gives back with `Release()`.
- **Structs by reference** (`const BufferDesc&`) are a `Ref<T>` to a named tuple with the C++
layout. That layout depends on the C++ build: `std::string`, for example, is 32 bytes with the
release CRT and 40 with the debug CRT.
- **Casting.** `<NvDevice>opaqueValue` is a plain pointer cast with no check: the object must
really be one.

## Tests

- `tslang/test/tester/tests/00cppvtable_class.ts` checks the slots, fields, `extends` and
`abstract` against a vtable made by hand. It is registered for AOT and JIT.
- `tslang/test/tester/cppvtable/*.ts` holds one compile error per rule above.
2 changes: 2 additions & 0 deletions tslang/include/TypeScript/Defines.h
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,8 @@
#define DLL_IMPORT "dllimport"
#define DLL_NAME "dllname"
#define LINK_NAME "linkname"
// A class laid over an existing C++ object: its vtable is the C++ one (see ClassInfo::isCppVTable)
#define CPP_VTABLE "cppvtable"

#if __LP64__
#define TRAMPOLINE_SIZE 48
Expand Down
20 changes: 15 additions & 5 deletions tslang/include/TypeScript/MLIRLogic/MLIRGenStore.h
Original file line number Diff line number Diff line change
Expand Up @@ -778,6 +778,11 @@ struct ClassInfo
bool isPublic;
bool isDynamicImport;
bool hasRTTI;
// `@cppvtable`: a binding to C++ objects (MSVC/Itanium single inheritance) made elsewhere. The
// instance is the C++ object, its first word the C++ vtable, and the class's methods are that
// vtable's slots from 0 in declaration order, a base class's first: no `.instanceOf`, `.new`,
// interface or static-field slots, no RTTI, and no vtable or constructor of its own.
bool isCppVTable;
ProcessingStages processingAtEvaluation;
ProcessingStages processing;
// the module `processing` was reached in: a discovery module is thrown away with what it holds
Expand All @@ -788,7 +793,7 @@ struct ClassInfo
ClassInfo()
: isDeclaration(false), hasNew(false), hasConstructor(false), constructorAccessLevel(mlir_ts::AccessLevel::Public), hasInitializers(false), hasStaticConstructor(false),
hasStaticInitializers(false), hasVirtualTable(false), isAbstract(false), isExport(false), isImport(false),
isPublic(false), isDynamicImport(false), hasRTTI(false),
isPublic(false), isDynamicImport(false), hasRTTI(false), isCppVTable(false),
processingAtEvaluation(ProcessingStages::NotSet), processing(ProcessingStages::NotSet)
{
}
Expand Down Expand Up @@ -901,7 +906,7 @@ struct ClassInfo
// (mlirGenInstanceOfOpaque). A root class puts it there ahead of its interfaces, and a
// derived class overrides it in place. Before this, a root class that implemented an
// interface kept the interface's vtable in slot 0, and `<B>anyValue` called it.
if (baseClasses.empty())
if (baseClasses.empty() && !isCppVTable)
{
auto instanceOf = std::find_if(methods.begin(), methods.end(),
[](auto &method) { return method.name == INSTANCEOF_NAME; });
Expand All @@ -912,7 +917,7 @@ struct ClassInfo
}
}

// do vtable for current class
// do vtable for current class (a @cppvtable class implements nothing, see isCppVTable)
for (auto &implement : implements)
{
if (interfaceSlots.contains(implement.interface->fullName))
Expand Down Expand Up @@ -943,6 +948,11 @@ struct ClassInfo
{
continue;
}
#else
if (method.isStatic && isCppVTable)
{
continue;
}
#endif

auto it = methodSlots.find(method.name);
Expand All @@ -966,8 +976,8 @@ struct ClassInfo
}

#ifdef ADD_STATIC_MEMBERS_TO_VTABLE
// static fields
for (auto &staticField : staticFields)
// static fields (a C++ vtable has none)
for (auto &staticField : isCppVTable ? llvm::MutableArrayRef<StaticFieldInfo>() : llvm::MutableArrayRef<StaticFieldInfo>(staticFields))
{
staticField.virtualIndex = vtable.size();
vtable.push_back({staticField, false});
Expand Down
5 changes: 5 additions & 0 deletions tslang/include/TypeScript/TargetInfo.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ struct TargetInfo
// Whether tslang.exe - an x64 process - can execute this target's code in-process.
bool supportsInProcessJit = true;

// MSVC C++ member functions are __thiscall on 32-bit x86 (`this` in ECX), not the plain C
// convention a class method is called with, so a @cppvtable class can't call them.
bool memberCallsUseThiscall = false;

static TargetInfo fromTriple(const llvm::Triple &target, const llvm::Triple &host)
{
TargetInfo info;
Expand All @@ -50,6 +54,7 @@ struct TargetInfo
info.stdcallDecoratesCxxThrow = msvc && x86_32;
info.supportsInProcessJit =
target.getArch() == host.getArch() && target.getOS() == host.getOS();
info.memberCallsUseThiscall = msvc && x86_32;

return info;
}
Expand Down
8 changes: 8 additions & 0 deletions tslang/lib/TypeScript/DeclarationPrinter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,14 @@ namespace typescript
printNamespaceBegin(classType->elementNamespace);

printBeforeDeclaration();
// as layout-relevant as `abstract` below: without it the importer numbers the slots of an
// ordinary class, `.instanceOf` first
if (classType->isCppVTable)
{
os << "@" CPP_VTABLE;
newline();
}

// `abstract` is layout-relevant, not just semantic: mlirGenClassNew skips
// synthesizing the `.new` method for abstract classes, so dropping the
// modifier makes the reimporting module insert a `.new` vtable slot the
Expand Down
9 changes: 9 additions & 0 deletions tslang/lib/TypeScript/MLIRGenCast.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,15 @@ namespace mlirgen
LLVM_DEBUG(llvm::dbgs() << "\n!! cast " << valueType << "\n -> " << type
<< "\n";);

// see isCppVTableClass: `any` is unboxed and an interface made by asking the object
if ((isa<mlir_ts::AnyType>(valueType) && isCppVTableClass(type))
|| ((isa<mlir_ts::AnyType>(type) || isa<mlir_ts::InterfaceType>(type)) && isCppVTableClass(valueType)))
{
emitError(location) << "can't cast '" << to_print(valueType) << "' to '" << to_print(type)
<< "': a @" CPP_VTABLE " class's objects are C++ objects, with no type information";
return mlir::failure();
}

if (mlir::failed(verifyCastPreconditions(location, type, valueType, disableStrictNullCheck)))
{
return mlir::failure();
Expand Down
Loading
Loading