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
16 changes: 14 additions & 2 deletions .github/actions/Build_and_Test_CppInterOp/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ inputs:
required: false
default: 'false'
description: 'matrix.wasm. Gates emscripten cross-build vs native steps.'
arch:
required: false
default: ''
description: 'matrix.arch. "x86" targets 32-bit on Windows.'

runs:
using: composite
Expand Down Expand Up @@ -262,9 +266,17 @@ runs:
$env:CPPINTEROP_BUILD_DIR="$env:PWD_DIR"
echo "CPPINTEROP_BUILD_DIR=$env:CPPINTEROP_BUILD_DIR"
echo "CPPINTEROP_BUILD_DIR=$env:CPPINTEROP_BUILD_DIR" >> $env:GITHUB_ENV
# arch=x86: 32-bit target on the VS generator with the 64-bit-
# hosted toolset. The LLVM cell consumed above must carry
# matching Win32 .libs or the link fails with LNK4272.
$PLATFORM_FLAGS = @()
if ( "${{ inputs.arch }}" -eq "x86" )
{
$PLATFORM_FLAGS = @("-A", "Win32", "-Thost=x64")
}
if ( "${{ inputs.cling }}" -imatch "On" )
{
cmake -DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE }} `
cmake @PLATFORM_FLAGS -DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE }} `
-DCPPINTEROP_USE_CLING=ON `
-DCPPINTEROP_USE_REPL=OFF `
-DCling_DIR="$env:CLING_CMAKE_DIR" `
Expand All @@ -274,7 +286,7 @@ runs:
}
else
{
cmake -DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE }} `
cmake @PLATFORM_FLAGS -DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE }} `
-DLLVM_DIR="$env:LLVM_BUILD_DIR\lib\cmake\llvm" `
-DLLVM_ENABLE_WERROR=On `
-DClang_DIR="$env:LLVM_BUILD_DIR\lib\cmake\clang" -DCODE_COVERAGE=${{ env.CODE_COVERAGE }} -DCMAKE_INSTALL_PREFIX="$env:CPPINTEROP_DIR" ..\
Expand Down
12 changes: 9 additions & 3 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ jobs:
# flavor: '' (default — pull llvm-release from
# ci-workflows' Releases cache)
# arch is derived from the os slug inside setup-llvm; pass an
# explicit `flavor-arch` only on hosts whose os name doesn't
# carry the architecture (none of GHA's runner images today).
# explicit `arch` only when the os name doesn't carry the
# architecture, or when the build targets something other than
# the runner's own arch (e.g win32: 'x86' = 32-bit MSVC).
# `flavor` selects the LLVM source per row:
# '' latest LLVM (cells.yaml carries the cell)
# system apt-llvm.org / brew llvm@N — for older majors
Expand Down Expand Up @@ -60,13 +61,16 @@ jobs:
- { name: osx26-x86-clang-llvm21-cppyy, os: macos-26-intel, compiler: clang, clang-runtime: '21', cppyy: On, flavor: system }
- { name: osx26-x86-clang-cling-llvm20-cppyy, os: macos-26-intel, compiler: clang, clang-runtime: '20', cling: 'On', cppyy: On, flavor: cling, flavor-version: 'ROOT-llvm20' }
# Windows
# No windows-11-arm row: x86 (win2025-msvc-llvm22) covers the
# No windows-11-arm row: x64 (win2025-msvc-llvm22) covers the
# same MSVC+LLVM22 surface, and the arm runner ships an x64
# MSVC environment by default. ci-workflows can publish arm64
# .libs once `arch: arm64` lands in install-build-deps and the
# poisoned arm64 cache entry is purged; restore the row then.
- { name: win2025-msvc-llvm22, os: windows-2025, compiler: msvc, clang-runtime: '22' }
- { name: win2025-msvc-cling-llvm20, os: windows-2025, compiler: msvc, clang-runtime: '20', cling: 'On', flavor: cling, flavor-version: 'ROOT-llvm20' }
# 32-bit MSVC target: consumes the Win32 llvm-release cell and
# builds CppInterOp with -A Win32 -Thost=x64
- { name: win2025-msvc32-llvm22, os: windows-2025, compiler: msvc, clang-runtime: '22', arch: x86 }

steps: &build_steps
- uses: actions/checkout@v6
Expand Down Expand Up @@ -106,6 +110,7 @@ jobs:
with:
version: ${{ matrix.clang-runtime }}
os: ${{ matrix.self-hosted-os || matrix.os }}
arch: ${{ matrix.arch }}
flavor: ${{ matrix.flavor }}
flavor-version: ${{ matrix.flavor-version }}

Expand All @@ -128,6 +133,7 @@ jobs:
documentation: ${{ matrix.documentation }}
sanitizer: ${{ matrix.sanitizer }}
valgrind: ${{ matrix.Valgrind }}
arch: ${{ matrix.arch }}

- name: Prepare code coverage report
if: ${{ success() && (matrix.coverage == true) }}
Expand Down
64 changes: 51 additions & 13 deletions lib/CppInterOp/CppInterOp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1584,7 +1584,7 @@ bool IsFunctionProtoType(ConstTypeRef TyRef) {
INTEROP_TRACE(TyRef);
QualType QT = QualType::getFromOpaquePtr(TyRef.data);
const auto* T = QT.getTypePtr();
return llvm::isa_and_nonnull<clang::FunctionProtoType>(T);
return INTEROP_RETURN(llvm::isa_and_nonnull<clang::FunctionProtoType>(T));
}

void GetFnTypeSignature(ConstTypeRef fn_type, std::vector<TypeRef>& sig) {
Expand Down Expand Up @@ -2133,6 +2133,38 @@ constexpr int kABIPrefixSize = 2;
static_assert(detail::kVTableOverlayPrefixSize == kABIPrefixSize + 1,
"public prefix size must equal ABI prefix + 1 hidden slot");

// Vtable slots hold member functions: on 32-bit MSVC that means __thiscall,
// which a free function cannot be declared as (C3865). Use a member of a
// base-less class instead; `this` is the object being destroyed. MSVC's
// deleting dtor takes (this, int flags), Itanium's D0 only `this`.
struct VTableOverlayDtorHost {
#ifdef _WIN32
void Wrapper(int flags);
#else
void Wrapper();
#endif
};
#ifdef _WIN32
using VTableOverlayDtorSlotFn = void (VTableOverlayDtorHost::*)(int);
#else
using VTableOverlayDtorSlotFn = void (VTableOverlayDtorHost::*)();
#endif

// A non-virtual mfp of a base-less class keeps the entry point in its first
// pointer-sized word on every ABI we target (MSVC: just the address;
// Itanium: {fnptr, adj} with adj = 0).
static VTableOverlayDtorSlotFn SlotToDtorFn(void* slot) {
static_assert(sizeof(VTableOverlayDtorSlotFn) >= sizeof(void*));
VTableOverlayDtorSlotFn fn{};
std::memcpy(&fn, static_cast<void*>(&slot), sizeof(slot));
return fn;
}
static void* DtorFnToSlot(VTableOverlayDtorSlotFn fn) {
void* slot;
std::memcpy(static_cast<void*>(&slot), &fn, sizeof(slot));
return slot;
}

// Single locus for vptr reads/writes and slot arithmetic in this file;
// the public header's VTableOverlayExtraSlot covers the symmetric pun
// thunks need on the read side. The owned block layout is:
Expand All @@ -2143,7 +2175,7 @@ static_assert(detail::kVTableOverlayPrefixSize == kABIPrefixSize + 1,
// address_point
//
// The wrapper at the deleting-dtor slot recovers its VTableOverlay from
// the hidden self-ptr slot via a fixed-offset load from `self`'s vptr.
// the hidden self-ptr slot via a fixed-offset load from `this`'s vptr.
struct VTableOverlay {
void** block; // owned, freed in ~VTableOverlay
void** original_vptr; // restored on caller-driven teardown
Expand All @@ -2152,7 +2184,7 @@ struct VTableOverlay {
bool dtor_fired = false; // wrapper started -- skip vptr restore
// Dtor-hook fields. orig_dtor stays null when the caller passed
// on_destroy = nullptr; the wrapper is then not installed at all.
void (*orig_dtor)(void*) = nullptr;
VTableOverlayDtorSlotFn orig_dtor = nullptr;
VTableOverlayDtorHook cleanup = nullptr;
void* cleanup_data = nullptr;

Expand Down Expand Up @@ -2200,21 +2232,29 @@ struct VTableOverlay {

// Wrapper installed in the deleting-dtor slot when MakeVTableOverlay was
// called with a non-null on_destroy. Recovers the owning VTableOverlay
// from `self`'s vptr (hidden-slot fixed-offset load) and runs the
// callback BEFORE the original destructor: `self` is alive at that
// from `this`'s vptr (hidden-slot fixed-offset load) and runs the
Comment thread
aaronj0 marked this conversation as resolved.
// callback BEFORE the original destructor: the object is alive at that
// point so the callback can inspect it, and after the original
// deleting-destructor returns memory has been freed.
extern "C" void cppinteropVTableOverlayDtorWrapper(void* self) {
void** vptr = VTableOverlay::ReadVPtr(self);
#ifdef _WIN32
void VTableOverlayDtorHost::Wrapper(int flags) {
#else
void VTableOverlayDtorHost::Wrapper() {
#endif
void** vptr = VTableOverlay::ReadVPtr(this);
VTableOverlay* ov = *reinterpret_cast<VTableOverlay**>(
vptr - detail::kVTableOverlayPrefixSize);
// Snapshot orig_dtor before user code runs: a misbehaving callback
// that destroys the overlay must not strand the C++ destructor.
auto orig_dtor = ov->orig_dtor;
ov->dtor_fired = true;
if (ov->cleanup)
ov->cleanup(self, ov->cleanup_data);
orig_dtor(self);
ov->cleanup(this, ov->cleanup_data);
#ifdef _WIN32
(this->*orig_dtor)(flags);
#else
(this->*orig_dtor)();
#endif
}

// Minimum slot count a polymorphic class can have from its address point:
Expand Down Expand Up @@ -2323,12 +2363,10 @@ MakeVTableOverlay(void* inst, ConstDeclRef base, const ConstFuncRef* methods,
// destruction could fire the wrapper with stale state.
if (on_destroy) {
void** vptr = ov->address_point();
ov->orig_dtor =
VTableOverlay::BitCastFn<void (*)(void*)>(vptr[kDeletingDtorSlot]);
ov->orig_dtor = SlotToDtorFn(vptr[kDeletingDtorSlot]);
ov->cleanup = on_destroy;
ov->cleanup_data = cleanup_data;
vptr[kDeletingDtorSlot] =
VTableOverlay::BitCastFn<void*>(&cppinteropVTableOverlayDtorWrapper);
vptr[kDeletingDtorSlot] = DtorFnToSlot(&VTableOverlayDtorHost::Wrapper);
}

return INTEROP_RETURN(ov);
Expand Down
71 changes: 71 additions & 0 deletions lib/CppInterOp/CppInterOpInterpreter.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@
#include <sched.h>
#include <unistd.h>
#endif
#if defined(_WIN32) && (defined(_M_IX86) || defined(__i386__))
#include "llvm/ExecutionEngine/Orc/AbsoluteSymbols.h"
#include "llvm/Support/DynamicLibrary.h"
#include <deque>
#endif
#include <algorithm>
#include <cstdio>
#include <memory>
Expand Down Expand Up @@ -144,6 +149,66 @@ inline clang::NamedDecl* Named(clang::Sema* S, const char* Name,

namespace CppInternal {

#if defined(_WIN32) && (defined(_M_IX86) || defined(__i386__))
/// COFF-i386 linker names carry decorations the ORC in-process resolver
/// never undoes: a '_' prefix on cdecl symbols, '@<argbytes>' suffixes on
/// stdcall ones, and '__imp_' pointer indirections for dllimport. ORC
/// strips a global prefix for MachO only, so JIT'd code referencing the
/// CRT (_memset, __CxxThrowException@8, ...) or libraries loaded at
/// runtime fails to materialize. Normalize the way cling's
/// platform::DLSym does and retry against the loaded modules.
class COFFi386SymbolGenerator : public llvm::orc::DefinitionGenerator {
public:
llvm::Error
tryToGenerate(llvm::orc::LookupState& LS, llvm::orc::LookupKind K,
llvm::orc::JITDylib& JD,
llvm::orc::JITDylibLookupFlags JDLookupFlags,
const llvm::orc::SymbolLookupSet& LookupSet) override {
llvm::orc::SymbolMap NewSymbols;
for (const auto& KV : LookupSet) {
llvm::StringRef Name = *KV.first;
// MSVC C++ manglings ('?...') take no i386 decoration; the default
// process generator already resolves them.
if (Name.empty() || Name.starts_with("?"))
continue;
if (void* Addr = lookupDecorated(Name))
NewSymbols[KV.first] = {llvm::orc::ExecutorAddr::fromPtr(Addr),
llvm::JITSymbolFlags::Exported};
}
if (NewSymbols.empty())
return llvm::Error::success();
return JD.define(llvm::orc::absoluteSymbols(std::move(NewSymbols)));
}

private:
void* lookupDecorated(llvm::StringRef Name) {
bool Dllimport = Name.consume_front("__imp_");
Name.consume_front("_"); // i386 cdecl prefix
std::string S = Name.str();
void* Addr = llvm::sys::DynamicLibrary::SearchForAddressOfSymbol(S.c_str());
if (!Addr) {
// stdcall '@<argbytes>' suffixes don't appear in export tables
// (vcruntime exports _CxxThrowException, not _CxxThrowException@8).
size_t At = S.rfind('@');
if (At != std::string::npos && At + 1 < S.size() &&
S.find_first_not_of("0123456789", At + 1) == std::string::npos) {
S.resize(At);
Addr = llvm::sys::DynamicLibrary::SearchForAddressOfSymbol(S.c_str());
}
}
if (Addr && Dllimport) {
// dllimport references bind to a pointer to the symbol, not the
// symbol itself; park the address in a stable slot.
m_ImportSlots.push_back(Addr);
return &m_ImportSlots.back();
}
return Addr;
}

std::deque<void*> m_ImportSlots;
};
#endif // _WIN32 && i386

/// CppInterOp Interpreter
///
class Interpreter {
Expand Down Expand Up @@ -245,6 +310,12 @@ class Interpreter {
return nullptr;
}

#if defined(_WIN32) && (defined(_M_IX86) || defined(__i386__))
// Forces executor creation, so install before any execution.
compat::getExecutionEngine(*CI)->getMainJITDylib().addGenerator(
std::make_unique<COFFi386SymbolGenerator>());
#endif

return std::make_unique<Interpreter>(std::move(CI), std::move(io_ctx),
outOfProcess);
}
Expand Down
2 changes: 2 additions & 0 deletions unittests/CppInterOp/TracingTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1169,6 +1169,8 @@ TEST_F(TracingTest, ReproducerCompilesViaInterpreter) {
auto FooType = Cpp::GetTypeFromScope(Foo);
ASSERT_NE(FooType, nullptr);

EXPECT_FALSE(Cpp::IsFunctionProtoType(FooType));

Cpp::GetName(Foo);
Cpp::SizeOf(Foo);

Expand Down
Loading
Loading