Skip to content

@cpp calls pass records by value and read them back from registers as each C++ ABI does - #578

Merged
ASDAlexander77 merged 1 commit into
mainfrom
cpp-records-by-value
Oct 14, 2026
Merged

ASDAlexander77 merged 1 commit into
mainfrom
cpp-records-by-value

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

A @cpp class can now call C++ virtual methods that take a trivially copyable struct by value, or return a small one in registers. tsbindgen binds such methods instead of skipping them.

tslang

LowerToLLVM (lowerCppCallArgs, on both call and invoke): a tuple argument of a @cpp call is copied into a zeroed slot and passed the way the target passes the record. TargetInfo::cppRecordAbi selects the rules:

ABI Up to 16 bytes Larger
MSVC x64 1, 2, 4 or 8 bytes: in a register, as an integer of that size; any other size: a pointer to the copy a pointer to the copy
SysV x86-64 integers and pointers: one or two 64-bit registers, if that many of the six remain (counted), else on the stack on the stack (byval)
AAPCS64 [N x i64], which the backend places all in registers or all on the stack a pointer to the copy

A tuple the Itanium ABI returns in registers is read back from i64 / { i64, i64 } / [2 x i64]. That now covers any trivial record of up to 16 bytes of integers and pointers, not only one or two 8-byte fields.

These are errors rather than wrong calls:

  • floating-point records under the Itanium ABI (SSE registers, HFAs);
  • an unmarked trivial record of more than 16 bytes returned (mark it @cppnontrivial);
  • records by value in non-virtual methods or in methods with a body.

tsbindgen

  • A virtual method's by-value record parameter is emitted as the record's own type, unless the record:
    • isn't trivially copyable (MSVC would destroy, say, a RefCountPtr in the callee and release a reference the caller never added),
    • is a union,
    • or has floating-point fields under the Itanium ABI.
  • Under the Itanium ABI, a record result of more than 16 bytes is @cppnontrivial.

docs/cpp-classes.md ("Structs passed by value") and the README document this, including the cases left out.

Tests

  • abi_host.cpp gains byValue (records of 4, 8, 12, 16 and 24 bytes, then an int), pair, flags (three bools) and r12 (12 bytes). It runs against MSVC in test-jit-cppvtable-abi and against g++/clang on Linux CI. The callee changes its by-value copies, and the test checks that the caller's tuples are unchanged.
  • test-compile-cppvtable-records-{msvc,x86_64,aarch64} check each ABI's call shape in the IR on any host.
  • itanium_float_param.ts and itanium_record.ts (now floats) cover the errors.
  • TsClangImporterTests gain RecordsPassedByValue and RecordsReturnedInMemoryOrFloatRegisters.
  • Local results: full suite 4085/4085 on Windows, plus the new tests.
  • Itanium x86-64 against g++ (WSL): all six checks pass. The same check compiled with pre-alpha101 silently got both by-value cases wrong.

Downstream

native_ts_graphics' tools/gen_nvrhi_classes.py hand-wrote 11 NVRHI methods per ABI because tsbindgen skipped them. With this change tsbindgen binds them all. A local build against this tslang passes nvrhi_direct and headless on D3D12 and Vulkan (-debug) and on Android x86_64 and arm64. That cleanup lands after a release.

🤖 Generated with Claude Code

… each C++ ABI does

A C++ (virtual) method taking a trivially copyable struct by value, or
returning one in registers, now works through a @cpp class, and
tsbindgen emits such methods instead of skipping them.

LowerToLLVM (lowerCppCallArgs, for call and invoke): a tuple argument of
a @cpp call is passed from a zeroed copy as the target passes the record
(TargetInfo::cppRecordAbi):
- MSVC x64: 1, 2, 4 or 8 bytes in a register as an integer of its size,
  any other by a pointer to the copy;
- SysV x86-64: up to 16 bytes of integers and pointers in one or two
  64-bit registers when that many of the six remain, else on the stack,
  as a bigger one is (byval);
- AAPCS64: up to 16 bytes as [N x i64] (registers all or none), a bigger
  one by a pointer to the copy.
A tuple the Itanium ABI returns in registers (any trivial record of up to
16 bytes of integers and pointers, not only one or two 8-byte fields) is
read back from i64 / {i64, i64} / [2 x i64]. Floating-point records on
the Itanium targets (SSE registers, HFAs) are an error, and so is a
trivial record of more than 16 bytes returned unmarked (@cppnontrivial).
MLIRGen: records by value in non-virtual methods and in methods with a
body are errors.

tsbindgen: a virtual method's by-value record parameter is its own type,
unless it isn't trivially copyable (MSVC would destroy a RefCountPtr in
the callee), a union, or (Itanium) has floating-point fields; a record
result of more than 16 bytes is @cppnontrivial under the Itanium ABI.

Tests: abi_host.cpp's byValue/pair/flags/r12 (records of 3 to 24 bytes),
run against MSVC (test-jit-cppvtable-abi) and, on Linux, g++; the IR of
each ABI's calls (test-compile-cppvtable-records-*); floating-point
errors; tsbindgen's unit tests. Verified against g++ on x86-64 too: the
same check compiled with pre-alpha101 fails both by-value cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit 43110f5 into main Oct 14, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the cpp-records-by-value branch October 14, 2026 15:42
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