From a4de780ca44313ec4255d0594c8a432c1ebeec94 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 11 Oct 2026 21:19:52 +0100 Subject: [PATCH 1/4] @cppvtable: methods returning a record on x86-64 Itanium (g++, clang) The Itanium ABI returns a record with a destructor or a copy constructor through a hidden pointer passed before 'this' - marked @cppnontrivial, as a tuple can't say so - and a trivial record of one or two 8-byte integer or pointer fields in RAX:RDX, as tslang returns such a tuple. On x86-64 a @cppvtable method returning a tuple now follows that: a @cppnontrivial method's slot is called as (Ref, this, args...), an unmarked one keeps its declared type, and any other record is an error rather than a wrong call. AArch64 (hidden pointer in X8) stays an error. DeclarationPrinter keeps @cppnontrivial for an importer. Tests: test-jit-cppvtable-abi calls the methods of an object made by the host's C++ compiler (cppvtable/abi_host.cpp, a shared library) - MSVC's ABI on Windows, g++'s on x86-64 Linux - with records with and without a destructor, of 8 and 16 bytes; errors for an unsupported record, @cppnontrivial on a method returning a number, and AArch64. Co-Authored-By: Claude Opus 5.5 --- docs/cppvtable-classes.md | 29 ++++-- tslang/include/TypeScript/Defines.h | 3 + .../TypeScript/MLIRLogic/MLIRGenStore.h | 5 +- tslang/include/TypeScript/TargetInfo.h | 8 ++ tslang/lib/TypeScript/DeclarationPrinter.cpp | 7 ++ tslang/lib/TypeScript/MLIRGenAccessCall.cpp | 22 ++++- tslang/lib/TypeScript/MLIRGenClasses.cpp | 91 +++++++++++++++++-- tslang/lib/TypeScript/MLIRGenImpl.h | 17 +++- tslang/test/tester/CMakeLists.txt | 46 +++++++++- tslang/test/tester/cppvtable/abi_host.cpp | 58 ++++++++++++ tslang/test/tester/cppvtable/abi_itanium.ts | 34 +++++++ tslang/test/tester/cppvtable/abi_msvc.ts | 32 +++++++ .../tester/cppvtable/hidden_return_target.ts | 4 +- .../test/tester/cppvtable/itanium_record.ts | 12 +++ .../tester/cppvtable/nontrivial_no_struct.ts | 11 +++ 15 files changed, 355 insertions(+), 24 deletions(-) create mode 100644 tslang/test/tester/cppvtable/abi_host.cpp create mode 100644 tslang/test/tester/cppvtable/abi_itanium.ts create mode 100644 tslang/test/tester/cppvtable/abi_msvc.ts create mode 100644 tslang/test/tester/cppvtable/itanium_record.ts create mode 100644 tslang/test/tester/cppvtable/nontrivial_no_struct.ts diff --git a/docs/cppvtable-classes.md b/docs/cppvtable-classes.md index d50e794db..5414aa9bf 100644 --- a/docs/cppvtable-classes.md +++ b/docs/cppvtable-classes.md @@ -60,14 +60,18 @@ these are errors and not warnings. | `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 method returning a tuple, on a non-MSVC target | the Itanium ABI returns records otherwise (see below) | +| a method returning a tuple, on a target other than MSVC and x86-64 Itanium | AArch64 passes the hidden result pointer in X8, not as an argument (see below) | +| on x86-64 Itanium, a method returning a trivial tuple other than one or two 8-byte integer or pointer fields | SysV packs it into registers by its fields, which a tuple isn't (see below) | 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 file.cpp`. Declare unused slots with + with `cl /c /Zs /d1reportSingleClassLayout file.cpp`; clang (Itanium) with + `clang++ -S -emit-llvm -o /dev/null -Xclang -fdump-vtable-layouts file.cpp`, for a class whose + vtable the file emits (it defines the first virtual method that isn't inline), where slot N is + entry N + 2 (entries 0 and 1 are the offset to top and the RTTI). 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()`). @@ -80,6 +84,7 @@ A generic or static class can't be `@cppvtable` either. ```ts type RefCountPtr = [pointer: Opaque | null]; // nvrhi::RefCountPtr + @cppnontrivial // it has a destructor createBuffer(desc: Ref): RefCountPtr; // const handle = device.createBuffer(Ref(desc)) ``` @@ -90,12 +95,22 @@ A generic or static class can't be `@cppvtable` either. instance methods". A method returning a class (`getLayout(): IBindingLayout`, a C++ pointer) or a number still returns it in a register. + The Itanium ABI (g++, clang) decides by the record, and tslang does it on x86-64: + + - a record with a destructor or a copy constructor - `RefCountPtr` - comes back through a + hidden pointer passed *before* `this`. A tuple can't say the C++ type has them, so mark the + method `@cppnontrivial`. (A trivial record of more than 16 bytes is returned the same way, so + `@cppnontrivial` is right for it too.) MSVC ignores the mark; + - a trivial record of one or two 8-byte integer or pointer fields - `nvrhi::Object` - comes + back in RAX:RDX, as tslang returns such a tuple: no mark; + - any other trivial record of up to 16 bytes is packed into registers by the SysV + classification, which isn't done: such a method is an error, as is any method returning a + tuple on another Itanium target (AArch64 passes the hidden pointer in X8). + The record isn't destroyed: a `RefCountPtr` the method returns holds a reference, which the - caller gives back with `Release()`. The Itanium ABI passes the pointer before `this`, and only - for some records, so on a non-MSVC target such a method is an error. A method stored in a - variable and called later takes the hidden pointer as its first argument; call it directly. - tslang pre-alpha94 and earlier need the hidden pointer declared as a parameter: - `createBuffer(ret: Ref, desc: Ref): Opaque`. + caller gives back with `Release()`. A method stored in a variable and called later takes the + hidden pointer as an argument; call it directly. tslang pre-alpha94 and earlier need the hidden + pointer declared as a parameter: `createBuffer(ret: Ref, desc: Ref): Opaque`. - **Structs by reference** (`const BufferDesc&`) are a `Ref` 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. diff --git a/tslang/include/TypeScript/Defines.h b/tslang/include/TypeScript/Defines.h index 1d89d1b43..55ffc134a 100644 --- a/tslang/include/TypeScript/Defines.h +++ b/tslang/include/TypeScript/Defines.h @@ -194,6 +194,9 @@ #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" +// A @cppvtable method's C++ return type has a destructor or a copy constructor (see +// mlirGenClassCppVTableHiddenReturn) +#define CPP_NON_TRIVIAL "cppnontrivial" #if __LP64__ #define TRAMPOLINE_SIZE 48 diff --git a/tslang/include/TypeScript/MLIRLogic/MLIRGenStore.h b/tslang/include/TypeScript/MLIRLogic/MLIRGenStore.h index 09dd23aa2..766115c93 100644 --- a/tslang/include/TypeScript/MLIRLogic/MLIRGenStore.h +++ b/tslang/include/TypeScript/MLIRLogic/MLIRGenStore.h @@ -152,9 +152,12 @@ struct MethodInfo // @dllname: exported symbol name, empty when the method keeps funcName std::string dllName; // A @cppvtable method returning a C++ record: the type it is declared with (funcType is the - // one its slot is called with, the hidden result pointer after `this`, see + // one its slot is called with, the hidden result pointer added, see // mlirGenClassCppVTableHiddenReturn); null for any other method. mlir_ts::FunctionType declaredFuncType; + // @cppnontrivial: the C++ record a @cppvtable method returns has a destructor or a copy + // constructor, which decides how the Itanium ABI returns it + bool cppNonTrivialReturn = false; }; struct GenericMethodInfo diff --git a/tslang/include/TypeScript/TargetInfo.h b/tslang/include/TypeScript/TargetInfo.h index a44108fce..d9632a165 100644 --- a/tslang/include/TypeScript/TargetInfo.h +++ b/tslang/include/TypeScript/TargetInfo.h @@ -38,6 +38,13 @@ struct TargetInfo // before `this`, and only for some records. bool hiddenReturnPointerAfterThis = false; + // The Itanium ABI on x86-64 returns a record with a destructor or a copy constructor (marked + // @cppnontrivial) through a hidden pointer passed before `this` - an ordinary first argument, + // in RDI - and returned in turn; a record of one or two 8-byte integer or pointer fields + // without them comes back in RAX:RDX, as a tuple is returned. (AArch64 passes the hidden + // pointer in X8, which an ordinary argument isn't, so it has neither.) + bool hiddenReturnPointerBeforeThis = false; + static TargetInfo fromTriple(const llvm::Triple &target, const llvm::Triple &host) { TargetInfo info; @@ -62,6 +69,7 @@ struct TargetInfo target.getArch() == host.getArch() && target.getOS() == host.getOS(); info.memberCallsUseThiscall = msvc && x86_32; info.hiddenReturnPointerAfterThis = msvc; + info.hiddenReturnPointerBeforeThis = !msvc && target.getArch() == llvm::Triple::x86_64; return info; } diff --git a/tslang/lib/TypeScript/DeclarationPrinter.cpp b/tslang/lib/TypeScript/DeclarationPrinter.cpp index 78c7a5638..0abf56bb2 100644 --- a/tslang/lib/TypeScript/DeclarationPrinter.cpp +++ b/tslang/lib/TypeScript/DeclarationPrinter.cpp @@ -646,6 +646,13 @@ namespace typescript newline(); } + if (method.cppNonTrivialReturn) + { + os.indent(4); + os << "@" CPP_NON_TRIVIAL; + newline(); + } + os.indent(4); if (method.accessLevel == mlir_ts::AccessLevel::Protected) diff --git a/tslang/lib/TypeScript/MLIRGenAccessCall.cpp b/tslang/lib/TypeScript/MLIRGenAccessCall.cpp index 52631faf9..6d7d3e576 100644 --- a/tslang/lib/TypeScript/MLIRGenAccessCall.cpp +++ b/tslang/lib/TypeScript/MLIRGenAccessCall.cpp @@ -1761,8 +1761,26 @@ namespace mlirgen builder.getIndexAttr(0)); SmallVector slotOperands; slotOperands.push_back(resultSlot); - slotOperands.append(operands.begin(), operands.end()); - auto callResult = mlirGenCallFunction(location, calledFuncType, unboundFuncRefValue, thisValue, slotOperands, genContext); + ValueOrLogicalResult callResult(mlir::failure()); + if (cppVTableHiddenReturnBeforeThis(funcRefValue)) + { + // the Itanium ABI: the slot is called as (Ref, this, args...) + SmallVector inputs; + inputs.push_back(calledFuncType.getInput(1)); + inputs.push_back(calledFuncType.getInput(0)); + inputs.append(calledFuncType.getInputs().begin() + 2, calledFuncType.getInputs().end()); + auto swappedFuncType = getFunctionType(inputs, calledFuncType.getResults(), calledFuncType.isVarArg()); + auto swappedFuncRefValue = builder.create(location, swappedFuncType, funcRefValue); + slotOperands.push_back(thisValue); + slotOperands.append(operands.begin(), operands.end()); + callResult = mlirGenCallFunction(location, swappedFuncType, swappedFuncRefValue, slotOperands, genContext); + } + else + { + slotOperands.append(operands.begin(), operands.end()); + callResult = mlirGenCallFunction(location, calledFuncType, unboundFuncRefValue, thisValue, slotOperands, genContext); + } + if (mlir::failed(callResult)) { value = mlir::failure(); diff --git a/tslang/lib/TypeScript/MLIRGenClasses.cpp b/tslang/lib/TypeScript/MLIRGenClasses.cpp index 621982dd6..9286080f0 100644 --- a/tslang/lib/TypeScript/MLIRGenClasses.cpp +++ b/tslang/lib/TypeScript/MLIRGenClasses.cpp @@ -376,11 +376,24 @@ namespace mlirgen // called as (this, Ref, args...) -> Ref, the type kept in the MethodInfo, and a call // (mlirGenCall) passes a local of T and reads it back. The declared type stays for // DeclarationPrinter, so an importer lowers it again. + // + // The Itanium ABI on x86-64 (clang's ItaniumCXXABI::classifyReturnType, then the SysV + // classification) passes the pointer before `this`, for a record with a destructor or a copy + // constructor - nonTrivial, the method's @cppnontrivial, as a tuple can't say so - and the call + // swaps the two. A trivial record of one or two 8-byte integer or pointer fields comes back in + // RAX:RDX, as tslang returns such a tuple, so the method keeps its declared type; other trivial + // records are packed into registers or returned in memory, which isn't done here. mlir::LogicalResult MLIRGenImpl::mlirGenClassCppVTableHiddenReturn(mlir::Location location, ClassInfo::TypePtr newClassPtr, - StringRef funcName) + StringRef funcName, bool nonTrivial) { if (!newClassPtr->isCppVTable) { + if (nonTrivial) + { + emitError(location) << "@" CPP_NON_TRIVIAL " is for a method of a @" CPP_VTABLE " class returning a struct"; + return mlir::failure(); + } + return mlir::success(); } @@ -393,17 +406,43 @@ namespace mlirgen auto funcType = method->funcType; if (funcType.getNumResults() != 1 || !isa(funcType.getResult(0)) || funcType.getNumInputs() < 1) { + if (nonTrivial) + { + emitError(location) << "@" CPP_NON_TRIVIAL " is for a method of a @" CPP_VTABLE " class returning a struct"; + return mlir::failure(); + } + return mlir::success(); } - if (!compileOptions.targetInfo.hiddenReturnPointerAfterThis) + auto resultType = funcType.getResult(0); + auto &targetInfo = compileOptions.targetInfo; + auto beforeThis = false; + method->cppNonTrivialReturn = nonTrivial; + if (targetInfo.hiddenReturnPointerBeforeThis) + { + if (!nonTrivial) + { + if (isCppRecordReturnedInIntegerRegisters(mlir::cast(resultType))) + { + return mlir::success(); + } + + emitError(location) << "on this target a method of a @" CPP_VTABLE " class returns a struct in registers by its fields, " + << "which is supported for one or two 8-byte integer or pointer fields; one with a destructor or a " + << "copy constructor is returned through a pointer: mark the method @" CPP_NON_TRIVIAL; + return mlir::failure(); + } + + beforeThis = true; + } + else if (!targetInfo.hiddenReturnPointerAfterThis) { - emitError(location) << "a method of a @" CPP_VTABLE " class returning a struct needs the MSVC ABI, which returns it " - << "through a pointer after 'this'"; + emitError(location) << "a method of a @" CPP_VTABLE " class returning a struct needs the MSVC ABI or the x86-64 Itanium ABI, " + << "which return it through a pointer next to 'this'"; return mlir::failure(); } - auto resultType = funcType.getResult(0); auto resultRefType = mlir_ts::RefType::get(resultType); SmallVector inputs; inputs.push_back(funcType.getInput(0)); @@ -413,9 +452,48 @@ namespace mlirgen method->declaredFuncType = funcType; method->funcType = getFunctionType(inputs, {resultRefType}, funcType.isVarArg()); cppVTableHiddenReturns[funcName] = resultType; + if (beforeThis) + { + cppVTableHiddenReturnsBeforeThis.insert(funcName); + } + return mlir::success(); } + // The SysV classification of a trivial C++ record, as far as tslang returns a tuple the same + // way: one or two eightbytes of class INTEGER, each a whole field (an LLVM struct of i64s and + // pointers comes back in RAX:RDX; smaller fields would be packed into one register). + bool MLIRGenImpl::isCppRecordReturnedInIntegerRegisters(mlir_ts::TupleType recordType) + { + auto fields = recordType.getFields(); + if (fields.empty() || fields.size() > 2) + { + return false; + } + + auto isEightbyteInteger = [&](mlir::Type type) { + if (auto intType = dyn_cast(type)) + { + return intType.getWidth() == 64; + } + + return isa(type); + }; + + return llvm::all_of(fields, [&](auto &field) { + auto type = field.type; + // Opaque | null: a pointer that may be null + if (auto unionType = dyn_cast(type)) + { + return llvm::all_of(unionType.getTypes(), [&](mlir::Type member) { + return isa(member) || isEightbyteInteger(member); + }); + } + + return isEightbyteInteger(type); + }); + } + // A @cppvtable class only names the slots of a vtable made by C++, so everything that would add // a slot, a field initializer or code of its own is an error, and so is a hierarchy mixing it // with ordinary classes: a slot number off by one calls the wrong C++ method, with no error. @@ -2449,7 +2527,8 @@ genContext); classMethodMemberInfo.setFuncOp(funcOp); if (classMethodMemberInfo.registerClassMethodMember(loc(funcLikeDeclaration), orderWeight, accessLevel)) { - if (mlir::failed(mlirGenClassCppVTableHiddenReturn(loc(funcLikeDeclaration), newClassPtr, funcOp.getName()))) + if (mlir::failed(mlirGenClassCppVTableHiddenReturn(loc(funcLikeDeclaration), newClassPtr, funcOp.getName(), + MLIRHelper::hasDecorator(funcLikeDeclaration, CPP_NON_TRIVIAL)))) { return mlir::failure(); } diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index 45d2168f3..60501bc72 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -5352,6 +5352,17 @@ class MLIRGenImpl return mlir::Type(); } + // Whether that hidden pointer goes before `this` (the Itanium ABI), not after it (MSVC's) + bool cppVTableHiddenReturnBeforeThis(mlir::Value funcRef) + { + if (auto virtualSymbolRef = funcRef.getDefiningOp()) + { + return cppVTableHiddenReturnsBeforeThis.contains(virtualSymbolRef.getIdentifier()); + } + + return false; + } + // A @cppvtable class's objects are C++ objects: their slot 0 is a C++ method (the destructor), // not `.instanceOf`, and the class has no RTTI, so nothing may ask one what it is - not // `instanceof`, nor boxing into `any` (unboxed by asking), nor an interface (made by asking). @@ -11219,7 +11230,9 @@ class MLIRGenImpl } mlir::LogicalResult mlirGenClassCppVTableHiddenReturn(mlir::Location location, ClassInfo::TypePtr newClassPtr, - StringRef funcName); + StringRef funcName, bool nonTrivial); + + bool isCppRecordReturnedInIntegerRegisters(mlir_ts::TupleType recordType); mlir::LogicalResult mlirGenClassCppVTableCheck(mlir::Location location, ClassLikeDeclaration classDeclarationAST, @@ -14091,6 +14104,8 @@ class MLIRGenImpl // the @cppvtable methods returning a C++ record (by function name): the record's type llvm::StringMap cppVTableHiddenReturns; + // those of them whose hidden pointer goes before `this` (the Itanium ABI) + llvm::StringSet<> cppVTableHiddenReturnsBeforeThis; llvm::ScopedHashTable fullNameGenericClassesMap; diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 82eee4173..5cf4ab5b3 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -2751,9 +2751,9 @@ add_test(NAME test-compile-cppvtable-memory-model-none set_tests_properties(test-compile-cppvtable-memory-model-none PROPERTIES FAIL_REGULAR_EXPRESSION "error:|Stack dump|Assertion failed") -# A method returning a record (tests/00cppvtable_hidden_return.ts) is MSVC's: the Itanium ABI -# returns a record through a pointer before `this`, and only some records, so it is an error -# anywhere else and its tests run on Windows only. +# A method returning a record: tests/00cppvtable_hidden_return.ts's slots, made by hand, return it as +# MSVC does, through a pointer after `this`, so they run on Windows only; the Itanium ABI's way is +# tested against C++ below (test-jit-cppvtable-abi). if (WIN32) tslang_add_test(NAME test-compile-00-cppvtable-hidden-return COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00cppvtable_hidden_return.ts") tslang_add_test(NAME test-jit-00-cppvtable-hidden-return COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00cppvtable_hidden_return.ts") @@ -2761,12 +2761,48 @@ if (WIN32) tslang_add_test(NAME test-jit-shared-export-import-cppvtable-hidden-return COMMAND test-runner -jit -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_cppvtable_hidden_return.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_cppvtable_hidden_return.ts") endif() add_test(NAME test-compile-cppvtable-hidden-return-target - COMMAND $ --emit=obj --no-default-lib -mm=none -mtriple=x86_64-unknown-linux-gnu + COMMAND $ --emit=obj --no-default-lib -mm=none -mtriple=aarch64-unknown-linux-gnu "${PROJECT_SOURCE_DIR}/test/tester/cppvtable/hidden_return_target.ts" -o "${CMAKE_CURRENT_BINARY_DIR}/cppvtable-hidden-return-target.obj") set_tests_properties(test-compile-cppvtable-hidden-return-target - PROPERTIES PASS_REGULAR_EXPRESSION "hidden_return_target\\.ts:7:5: error: a method of a @cppvtable class returning a struct needs the MSVC ABI" + PROPERTIES PASS_REGULAR_EXPRESSION "hidden_return_target\\.ts:7:5: error: a method of a @cppvtable class returning a struct needs the MSVC ABI or the x86-64 Itanium ABI" + FAIL_REGULAR_EXPRESSION "Stack dump|Assertion failed") +# x86-64 Itanium: a trivial record tslang can't return as the SysV classification does, unmarked +add_test(NAME test-compile-cppvtable-itanium-record + COMMAND $ --emit=obj --no-default-lib -mm=none -mtriple=x86_64-unknown-linux-gnu + "${PROJECT_SOURCE_DIR}/test/tester/cppvtable/itanium_record.ts" + -o "${CMAKE_CURRENT_BINARY_DIR}/cppvtable-itanium-record.obj") +set_tests_properties(test-compile-cppvtable-itanium-record + PROPERTIES PASS_REGULAR_EXPRESSION "itanium_record\\.ts:7:5: error: on this target a method of a @cppvtable class returns a struct in registers by its fields.*mark the method @cppnontrivial" FAIL_REGULAR_EXPRESSION "Stack dump|Assertion failed") +add_test(NAME test-compile-cppvtable-nontrivial-no-struct + COMMAND $ --emit=obj --no-default-lib -mm=none + "${PROJECT_SOURCE_DIR}/test/tester/cppvtable/nontrivial_no_struct.ts" + -o "${CMAKE_CURRENT_BINARY_DIR}/cppvtable-nontrivial-no-struct.obj") +set_tests_properties(test-compile-cppvtable-nontrivial-no-struct + PROPERTIES PASS_REGULAR_EXPRESSION "nontrivial_no_struct\\.ts:[0-9]+:[0-9]+: error: @cppnontrivial is for a method of a @cppvtable class returning a struct" + FAIL_REGULAR_EXPRESSION "Stack dump|Assertion failed") + +# A @cppvtable class calling the methods of an object C++ made (cppvtable/abi_host.cpp), built by +# the host's compiler: its vtable, and the way its methods return records, are that compiler's ABI - +# MSVC's (abi_msvc.ts), or x86-64 Itanium's for g++ and clang (abi_itanium.ts) - not a vtable made +# by hand in tslang, which follows tslang's own idea of the ABI. +if (CMAKE_SIZEOF_VOID_P EQUAL 8 AND (MSVC OR (NOT WIN32 AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64|amd64)$"))) + add_library(tslang_cppvtable_abi SHARED cppvtable/abi_host.cpp) + if (MSVC) + set(cppvtable_abi msvc) + else() + set(cppvtable_abi itanium) + endif() + add_test(NAME test-jit-cppvtable-abi + COMMAND $ --emit=jit --no-default-lib -mm=none + --shared-libs=$ + --shared-libs=$ + "${PROJECT_SOURCE_DIR}/test/tester/cppvtable/abi_${cppvtable_abi}.ts") + set_tests_properties(test-jit-cppvtable-abi + PROPERTIES PASS_REGULAR_EXPRESSION "done\\." + FAIL_REGULAR_EXPRESSION "error:|Stack dump|Segmentation|Assertion failed") +endif() # MSVC member functions are __thiscall on 32-bit x86, which a class method call isn't. add_test(NAME test-compile-cppvtable-thiscall-target diff --git a/tslang/test/tester/cppvtable/abi_host.cpp b/tslang/test/tester/cppvtable/abi_host.cpp new file mode 100644 index 000000000..0c2b62758 --- /dev/null +++ b/tslang/test/tester/cppvtable/abi_host.cpp @@ -0,0 +1,58 @@ +// A C++ object whose methods a @cppvtable class calls (abi_msvc.ts, abi_itanium.ts): built by the +// host's C++ compiler, so its vtable and the way its methods return records are that compiler's +// ABI - MSVC's, or the Itanium one of g++ and clang - not a vtable made by hand in tslang. + +#include + +#ifdef _WIN32 +#define ABI_EXPORT extern "C" __declspec(dllexport) +#else +#define ABI_EXPORT extern "C" __attribute__((visibility("default"))) +#endif + +// non-trivial, as nvrhi::RefCountPtr: the Itanium ABI returns it through a pointer before `this` +struct Ptr +{ + void *p; + Ptr(void *v) : p(v) {} + Ptr(const Ptr &other) : p(other.p) {} + ~Ptr() {} +}; + +// trivial, 8 bytes, as nvrhi::Object: RAX +struct Obj +{ + void *p; +}; + +// trivial, 16 bytes: RAX:RDX +struct Two +{ + uint64_t a; + void *b; +}; + +static char storage[64]; + +// MSVC returns every one of these records through a pointer after `this` +struct Methods +{ + virtual ~Methods() {} + virtual Ptr make(int x) { return Ptr(&storage[x]); } + virtual Obj obj(int x) { return Obj{&storage[x + 1]}; } + virtual Two two(int x) { return Two{0x1234567890ull + x, &storage[x + 2]}; } + virtual int plain(int a, int b) { return a * b + value; } + int value = 7; +}; + +ABI_EXPORT Methods *abi_object() +{ + static Methods methods; + return &methods; +} + +// whether p is &storage[index] +ABI_EXPORT int abi_is_storage(void *p, int index) +{ + return p == &storage[index] ? 1 : 0; +} diff --git a/tslang/test/tester/cppvtable/abi_itanium.ts b/tslang/test/tester/cppvtable/abi_itanium.ts new file mode 100644 index 000000000..85a5cfebe --- /dev/null +++ b/tslang/test/tester/cppvtable/abi_itanium.ts @@ -0,0 +1,34 @@ +// The methods of a C++ object made by g++ or clang on x86-64 (abi_host.cpp): two destructor slots; +// a record with a destructor returned through a pointer before `this` (@cppnontrivial), trivial +// records of one or two 8-byte fields in RAX:RDX. + +type Ptr = [p: Opaque | null]; +type Obj = [p: Opaque | null]; +type Two = [a: u64, b: Opaque | null]; + +declare function abi_object(): Opaque; +declare function abi_is_storage(p: Opaque | null, index: s32): s32; + +@cppvtable +class Methods { + value: s32; + completeDestructor(): void; // slot 0 + deletingDestructor(): void; // slot 1 + @cppnontrivial + make(x: s32): Ptr; // slot 2 + obj(x: s32): Obj; // slot 3 + two(x: s32): Two; // slot 4 + plain(a: s32, b: s32): s32; // slot 5 +} + +function main() { + const methods = abi_object(); + assert(methods.value == 7, "the first field follows the vtable pointer"); + assert(abi_is_storage(methods.make(3).p, 3) == 1, "a record with a destructor"); + assert(abi_is_storage(methods.obj(4).p, 5) == 1, "a trivial 8-byte record"); + const two = methods.two(7); + assert(two.a == 0x1234567890 + 7 && abi_is_storage(two.b, 9) == 1, "a trivial 16-byte record"); + assert(methods.plain(6, 7) == 49, "a number"); + + print("done."); +} diff --git a/tslang/test/tester/cppvtable/abi_msvc.ts b/tslang/test/tester/cppvtable/abi_msvc.ts new file mode 100644 index 000000000..afca0c2c7 --- /dev/null +++ b/tslang/test/tester/cppvtable/abi_msvc.ts @@ -0,0 +1,32 @@ +// The methods of a C++ object made by MSVC (abi_host.cpp): one destructor slot, and every record +// returned through a pointer after `this`. + +type Ptr = [p: Opaque | null]; +type Obj = [p: Opaque | null]; +type Two = [a: u64, b: Opaque | null]; + +declare function abi_object(): Opaque; +declare function abi_is_storage(p: Opaque | null, index: s32): s32; + +@cppvtable +class Methods { + value: s32; + destructor(): void; // slot 0: the scalar deleting destructor + @cppnontrivial + make(x: s32): Ptr; // slot 1 + obj(x: s32): Obj; // slot 2 + two(x: s32): Two; // slot 3 + plain(a: s32, b: s32): s32; // slot 4 +} + +function main() { + const methods = abi_object(); + assert(methods.value == 7, "the first field follows the vtable pointer"); + assert(abi_is_storage(methods.make(3).p, 3) == 1, "a record with a destructor"); + assert(abi_is_storage(methods.obj(4).p, 5) == 1, "a trivial 8-byte record"); + const two = methods.two(7); + assert(two.a == 0x1234567890 + 7 && abi_is_storage(two.b, 9) == 1, "a trivial 16-byte record"); + assert(methods.plain(6, 7) == 49, "a number"); + + print("done."); +} diff --git a/tslang/test/tester/cppvtable/hidden_return_target.ts b/tslang/test/tester/cppvtable/hidden_return_target.ts index b03dc526d..11b67e3d8 100644 --- a/tslang/test/tester/cppvtable/hidden_return_target.ts +++ b/tslang/test/tester/cppvtable/hidden_return_target.ts @@ -1,5 +1,5 @@ -// Compiled for Linux: the Itanium ABI returns a record from a method otherwise than MSVC, through -// a pointer before `this`, and only some records. +// Compiled for AArch64 Linux: the Itanium ABI passes the hidden pointer a record is returned +// through in X8 there, which an ordinary argument isn't. type Pair = [first: s32, second: s32]; @cppvtable diff --git a/tslang/test/tester/cppvtable/itanium_record.ts b/tslang/test/tester/cppvtable/itanium_record.ts new file mode 100644 index 000000000..1c05d1329 --- /dev/null +++ b/tslang/test/tester/cppvtable/itanium_record.ts @@ -0,0 +1,12 @@ +// Compiled for x86-64 Linux: a trivial record of two 4-byte fields comes back packed into RAX, +// which a tuple of them isn't; without @cppnontrivial it is an error, not a wrong call. +type Pair = [first: s32, second: s32]; + +@cppvtable +class Methods { + pair(x: s32): Pair; +} + +function main() { + print("done."); +} diff --git a/tslang/test/tester/cppvtable/nontrivial_no_struct.ts b/tslang/test/tester/cppvtable/nontrivial_no_struct.ts new file mode 100644 index 000000000..05d7c0159 --- /dev/null +++ b/tslang/test/tester/cppvtable/nontrivial_no_struct.ts @@ -0,0 +1,11 @@ +// @cppnontrivial says how a returned C++ record is returned: on a method returning a number it is +// a mistake. +@cppvtable +class Methods { + @cppnontrivial + count(): s32; +} + +function main() { + print("done."); +} From 38fc24488d413da96e422428faf87f93b8ccc3d3 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 11 Oct 2026 21:26:16 +0100 Subject: [PATCH 2/4] @cppvtable: Itanium record returns on AArch64 too - the hidden pointer marked sret AArch64 passes the hidden pointer a record is returned through in X8, not as the first argument. The call now marks it sret (ts.sret_arg on CallIndirect, carried to CallInternal or Invoke, turned into llvm.sret by LowerToLLVM), so the backend passes it where the ABI does: X8 on AArch64, RDI on x86-64 as before. MSVC's pointer after 'this' stays an ordinary argument (Windows ARM64 passes it in X1). Checked against NDK clang's code for the same C++ calls on aarch64-linux-android (X8 = the result, X0 = this, X0:X1 for a 16-byte trivial record), and again with g++ on x86-64 Linux (WSL). Co-Authored-By: Claude Opus 5.5 --- docs/cppvtable-classes.md | 20 +++---- tslang/include/TypeScript/Defines.h | 4 ++ tslang/include/TypeScript/TargetInfo.h | 12 ++--- tslang/lib/TypeScript/LowerToAffineLoops.cpp | 14 ++++- tslang/lib/TypeScript/LowerToLLVM.cpp | 52 ++++++++++++++++++- tslang/lib/TypeScript/MLIRGenAccessCall.cpp | 10 ++++ tslang/lib/TypeScript/MLIRGenClasses.cpp | 23 ++++---- tslang/test/tester/CMakeLists.txt | 10 ++-- .../tester/cppvtable/hidden_return_target.ts | 4 +- 9 files changed, 113 insertions(+), 36 deletions(-) diff --git a/docs/cppvtable-classes.md b/docs/cppvtable-classes.md index 5414aa9bf..4ca960dee 100644 --- a/docs/cppvtable-classes.md +++ b/docs/cppvtable-classes.md @@ -60,8 +60,8 @@ these are errors and not warnings. | `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 method returning a tuple, on a target other than MSVC and x86-64 Itanium | AArch64 passes the hidden result pointer in X8, not as an argument (see below) | -| on x86-64 Itanium, a method returning a trivial tuple other than one or two 8-byte integer or pointer fields | SysV packs it into registers by its fields, which a tuple isn't (see below) | +| a method returning a tuple, on a target other than MSVC and Itanium x86-64 / AArch64 | each ABI returns records its own way (see below) | +| on Itanium, a method returning a trivial tuple other than one or two 8-byte integer or pointer fields | SysV / AAPCS64 pack it into registers by its fields, which a tuple isn't (see below) | A generic or static class can't be `@cppvtable` either. @@ -95,17 +95,19 @@ A generic or static class can't be `@cppvtable` either. instance methods". A method returning a class (`getLayout(): IBindingLayout`, a C++ pointer) or a number still returns it in a register. - The Itanium ABI (g++, clang) decides by the record, and tslang does it on x86-64: + The Itanium ABI (g++, clang) decides by the record, and tslang does it on x86-64 and AArch64 + (Linux, Android): - a record with a destructor or a copy constructor - `RefCountPtr` - comes back through a - hidden pointer passed *before* `this`. A tuple can't say the C++ type has them, so mark the - method `@cppnontrivial`. (A trivial record of more than 16 bytes is returned the same way, so - `@cppnontrivial` is right for it too.) MSVC ignores the mark; + hidden pointer passed *before* `this`, as the call's `sret` argument (in RDI on x86-64, X8 on + AArch64). A tuple can't say the C++ type has them, so mark the method `@cppnontrivial`. (A + trivial record of more than 16 bytes is returned the same way, so `@cppnontrivial` is right + for it too.) MSVC ignores the mark; - a trivial record of one or two 8-byte integer or pointer fields - `nvrhi::Object` - comes - back in RAX:RDX, as tslang returns such a tuple: no mark; - - any other trivial record of up to 16 bytes is packed into registers by the SysV + back in RAX:RDX / X0:X1, as tslang returns such a tuple: no mark; + - any other trivial record of up to 16 bytes is packed into registers by the SysV / AAPCS64 classification, which isn't done: such a method is an error, as is any method returning a - tuple on another Itanium target (AArch64 passes the hidden pointer in X8). + tuple on another Itanium target (32-bit x86, say). The record isn't destroyed: a `RefCountPtr` the method returns holds a reference, which the caller gives back with `Release()`. A method stored in a variable and called later takes the diff --git a/tslang/include/TypeScript/Defines.h b/tslang/include/TypeScript/Defines.h index 55ffc134a..d06c46aef 100644 --- a/tslang/include/TypeScript/Defines.h +++ b/tslang/include/TypeScript/Defines.h @@ -197,6 +197,10 @@ // A @cppvtable method's C++ return type has a destructor or a copy constructor (see // mlirGenClassCppVTableHiddenReturn) #define CPP_NON_TRIVIAL "cppnontrivial" +// On a call (CallIndirect, then CallInternal or Invoke): the index of the argument that is the hidden +// pointer the callee returns a record through, which LowerToLLVM marks `sret` for the target to pass +// it where the C++ ABI does (X8 on AArch64) +#define SRET_ARG_ATTR_NAME "ts.sret_arg" #if __LP64__ #define TRAMPOLINE_SIZE 48 diff --git a/tslang/include/TypeScript/TargetInfo.h b/tslang/include/TypeScript/TargetInfo.h index d9632a165..4f5f21213 100644 --- a/tslang/include/TypeScript/TargetInfo.h +++ b/tslang/include/TypeScript/TargetInfo.h @@ -38,11 +38,10 @@ struct TargetInfo // before `this`, and only for some records. bool hiddenReturnPointerAfterThis = false; - // The Itanium ABI on x86-64 returns a record with a destructor or a copy constructor (marked - // @cppnontrivial) through a hidden pointer passed before `this` - an ordinary first argument, - // in RDI - and returned in turn; a record of one or two 8-byte integer or pointer fields - // without them comes back in RAX:RDX, as a tuple is returned. (AArch64 passes the hidden - // pointer in X8, which an ordinary argument isn't, so it has neither.) + // The Itanium ABI on x86-64 and AArch64 returns a record with a destructor or a copy + // constructor (marked @cppnontrivial) through a hidden pointer, passed before `this` and marked + // `sret` (RDI on x86-64, X8 on AArch64); a record of one or two 8-byte integer or pointer fields + // without them comes back in RAX:RDX / X0:X1, as a tuple is returned. bool hiddenReturnPointerBeforeThis = false; static TargetInfo fromTriple(const llvm::Triple &target, const llvm::Triple &host) @@ -69,7 +68,8 @@ struct TargetInfo target.getArch() == host.getArch() && target.getOS() == host.getOS(); info.memberCallsUseThiscall = msvc && x86_32; info.hiddenReturnPointerAfterThis = msvc; - info.hiddenReturnPointerBeforeThis = !msvc && target.getArch() == llvm::Triple::x86_64; + info.hiddenReturnPointerBeforeThis = + !msvc && (target.getArch() == llvm::Triple::x86_64 || target.getArch() == llvm::Triple::aarch64); return info; } diff --git a/tslang/lib/TypeScript/LowerToAffineLoops.cpp b/tslang/lib/TypeScript/LowerToAffineLoops.cpp index d591ac238..5c3ad4377 100644 --- a/tslang/lib/TypeScript/LowerToAffineLoops.cpp +++ b/tslang/lib/TypeScript/LowerToAffineLoops.cpp @@ -2220,8 +2220,14 @@ struct CallIndirectOpLowering : public TsPattern : op.getOperands()) llvm::dbgs() << "!! ...call -> invoke operands: " << opit << "\n";); + auto sretArg = op->getAttr(SRET_ARG_ATTR_NAME); auto res = rewriter.replaceOpWithNewOp( op, op.getResultTypes(), op.getOperands(), continuationBlock, ValueRange{}, unwind, ValueRange{}); + if (sretArg) + { + res->setAttr(SRET_ARG_ATTR_NAME, sretArg); + } + return success(); } @@ -2230,7 +2236,13 @@ struct CallIndirectOpLowering : public TsPattern } // just replace - rewriter.replaceOpWithNewOp(op, op.getResultTypes(), op.getOperands()); + auto sretArg = op->getAttr(SRET_ARG_ATTR_NAME); + auto callInternalOp = rewriter.replaceOpWithNewOp(op, op.getResultTypes(), op.getOperands()); + if (sretArg) + { + callInternalOp->setAttr(SRET_ARG_ATTR_NAME, sretArg); + } + return success(); } }; diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index 4802704f0..4a060f1cf 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -2130,6 +2130,35 @@ struct SymbolCallInternalOpLowering : public TsLlvmPattern