diff --git a/core/metacling/src/TCling.cxx b/core/metacling/src/TCling.cxx index c1192d2235cf6..18cec76644027 100644 --- a/core/metacling/src/TCling.cxx +++ b/core/metacling/src/TCling.cxx @@ -1588,7 +1588,8 @@ TCling::TCling(const char *name, const char *title, const char* const argv[], vo // Tell CppInterOp that the cling::Interpreter instance is managed externally by ROOT // Sets the interpreter by passing the fInterpreter handle as soon as TCling is initialized - Cpp::UseExternalInterpreter((Cpp::TInterp_t*)fInterpreter.get()); + if (!IsFromRootCling()) + Cpp::UseExternalInterpreter(fInterpreter.get()); // Don't check whether modules' files exist. fInterpreter->getCI()->getPreprocessorOpts().DisablePCHOrModuleValidation = diff --git a/core/metacling/src/TClingCallFunc.cxx b/core/metacling/src/TClingCallFunc.cxx index 7ac8f8b879c55..c8337c1d190c8 100644 --- a/core/metacling/src/TClingCallFunc.cxx +++ b/core/metacling/src/TClingCallFunc.cxx @@ -1254,7 +1254,7 @@ void *TClingCallFunc::ExecDefaultConstructor(const TClingClassInfo *info, if (Cpp::IsClass(D) || Cpp::IsConstructor(D)) { R__LOCKGUARD_CLING(gInterpreterMutex); - return Cpp::Construct(D, address, nary); + return Cpp::Construct(D, address, nary).data; } ::Error("TClingCallFunc::ExecDefaultConstructor", "ClassInfo missing a valid Scope/Constructor"); @@ -1271,7 +1271,7 @@ void TClingCallFunc::ExecDestructor(const TClingClassInfo *info, void *address / R__LOCKGUARD_CLING(gInterpreterMutex); - if (Cpp::Destruct(address, info->GetDecl(), nary, withFree)) + if (Cpp::Destruct(address, const_cast(info->GetDecl()), withFree, nary)) return; ::Error("TClingCallFunc::ExecDestructor", "Called with no wrapper, not implemented!"); diff --git a/interpreter/CMakeLists.txt b/interpreter/CMakeLists.txt index 3ffed12d6b174..38b6fa149416c 100644 --- a/interpreter/CMakeLists.txt +++ b/interpreter/CMakeLists.txt @@ -600,6 +600,7 @@ set(cppinterop_src_files ${CMAKE_CURRENT_SOURCE_DIR}/CppInterOp/include/CppInterOp/CppInterOp.h ${CMAKE_CURRENT_SOURCE_DIR}/CppInterOp/include/CppInterOp/CppInterOpTypes.h ${CMAKE_CURRENT_SOURCE_DIR}/CppInterOp/include/CppInterOp/Dispatch.h + ${CMAKE_CURRENT_SOURCE_DIR}/CppInterOp/include/CppInterOp/Box.h ${cppinterop_gen_dir}/BuildInfo.inc ${cppinterop_gen_dir}/CppInterOpAPI.inc ${cppinterop_gen_dir}/CppInterOpDecl.inc diff --git a/interpreter/CppInterOp/.bazelrc b/interpreter/CppInterOp/.bazelrc new file mode 100644 index 0000000000000..b63aa0dff520f --- /dev/null +++ b/interpreter/CppInterOp/.bazelrc @@ -0,0 +1,6 @@ +common --enable_bzlmod +test --test_output=errors + +# The C++ toolchain (clang from the LLVM tree) and all ABI-critical flags are +# centralized in the cppyy_bazel module, which registers @llvm//:cc_toolchain. +# No per-repo compiler wiring needed. diff --git a/interpreter/CppInterOp/.bazelversion b/interpreter/CppInterOp/.bazelversion new file mode 100644 index 0000000000000..56b6be4ebb2fb --- /dev/null +++ b/interpreter/CppInterOp/.bazelversion @@ -0,0 +1 @@ +8.3.1 diff --git a/interpreter/CppInterOp/.gitignore b/interpreter/CppInterOp/.gitignore index a079a11bce9c2..69733a901df51 100644 --- a/interpreter/CppInterOp/.gitignore +++ b/interpreter/CppInterOp/.gitignore @@ -39,3 +39,11 @@ install # Default Virtual Environments .venv + +# Bazel (convenience symlinks; bazel-support/ is checked in) +/bazel-bin +/bazel-out +/bazel-testlogs +/bazel-CppInterOp +# Registry-specific; consumers regenerate. Not vendored upstream. +/MODULE.bazel.lock diff --git a/interpreter/CppInterOp/BUILD.bazel b/interpreter/CppInterOp/BUILD.bazel new file mode 100644 index 0000000000000..dd26bb5a1d9c3 --- /dev/null +++ b/interpreter/CppInterOp/BUILD.bazel @@ -0,0 +1,432 @@ +"""Experimental Bazel build for CppInterOp. CMake is the supported build; +this translates the CMake recipe to build libclangCppInterOp against a local +LLVM tree (selected via the LLVM_DIR env var, see @cppyy_bazel//:llvm.bzl).""" + +load("@bazel_skylib//rules:common_settings.bzl", "bool_flag", "string_flag") +load("@cppyy_bazel//:defs.bzl", "BASE_COPTS", "CPPINTEROP_COPTS", "DEFAULT_LTO_OPT_LEVEL", "LTO_OPT_LEVELS", "is_main_repo", "jit_cxx_interp_args", "llvm_linkopts", "llvm_lto_opt_linkopts", "llvm_system_libs", "llvm_tblgen_linkopts", "repo_rloc") +load( + "@cppyy_bazel//:rules.bzl", + "cppinterop_buildinfo_inc", + "cppinterop_cc_test", + "cppinterop_cxx_shim", + "cppinterop_dispatch_cc_test", + "cppinterop_tblgen_inc_files", +) + +# libclangCppInterOp is a sanitizer boundary: it's dlopen'd by a (possibly +# non-instrumented) host interpreter and works directly with the non-sanitized +# LLVM/clang libraries, so an asan/ubsan-instrumented build would die on +# undefined __asan_* symbols at load. Opt the package out; harmless when the +# consumer build isn't sanitized. +package( + default_visibility = ["//visibility:public"], + features = [ + "-asan", + "-ubsan", + ], +) + +# LTO opt level for the .so link on an LTO/bitcode @llvm tree (no-op otherwise). +# Governs clang's in-.so constexpr-evaluator stack frames; a heavy-JIT consumer +# raises it (--@cppinterop//:lto_opt_level=2). See defs.bzl LTO_OPT_LEVELS. +string_flag( + name = "lto_opt_level", + build_setting_default = DEFAULT_LTO_OPT_LEVEL, + values = LTO_OPT_LEVELS, +) + +[ + config_setting( + name = "lto_opt_O" + lvl, + flag_values = {":lto_opt_level": lvl}, + ) + for lvl in LTO_OPT_LEVELS +] + +# The JIT C++ toolchain the unit tests need at RUN time (clang-repl compiles +# in-process). Default empty -> standalone uses the host. A consumer without one +# stages headers/clang into runfiles here, paired with :jit_cxx_interp_args: +# --@cppinterop//:jit_cxx_data=//my/pkg:cppjit_jit_cxx_data +label_flag( + name = "jit_cxx_data", + build_setting_default = ":jit_cxx_data_default", +) + +filegroup( + name = "jit_cxx_data_default", + srcs = [], +) + +# The matching --gcc-toolchain / -stdlib++-isystem args (CPPINTEROP_JIT_CXX_ARGS +# make-var, appended to the tests' interpreter env). Default empty; a consumer +# staging :jit_cxx_data overrides with its own jit_cxx_interp_args() target: +# --@cppinterop//:jit_cxx_interp_args=//my/pkg:cppjit_jit_cxx_interp_args +label_flag( + name = "jit_cxx_interp_args", + build_setting_default = ":jit_cxx_interp_args_default", +) + +jit_cxx_interp_args( + name = "jit_cxx_interp_args_default", + args = "", +) + +# LLVM's --system-libs (zlib/zstd) for the self-contained link. Default: bare -l +# from the host; a hermetic consumer overrides with a cc_library shipping the .so's: +# --@cppinterop//:llvm_system_libs=@my_conda//:zlib_zstd +# NOTE: cc_shared_library does NOT propagate a cc_library dep's bare -l linkopts +# into the .so link (it does propagate .so-file srcs). So for the DEFAULT host +# case the bare -l must ride on the solib's own user_link_flags -- see +# :inline_system_libs below. This target still feeds the cc_binary/cc_test +# consumers (tblgen, the unit tests), which DO honor cc_library linkopts. +label_flag( + name = "llvm_system_libs", + build_setting_default = ":llvm_system_libs_default", +) + +cc_library( + name = "llvm_system_libs_default", + linkopts = llvm_system_libs(), +) + +# Whether to inline LLVM's bare -l --system-libs directly onto the solib's +# user_link_flags. True (default) for a host build where -lz/-lzstd resolve from +# the system. A hermetic consumer that ships those libs as .so files via the +# :llvm_system_libs cc_library (which DOES propagate into cc_shared_library) sets +# this False to avoid an unresolvable bare -l under remote execution: +# --@cppinterop//:inline_system_libs=false +bool_flag( + name = "inline_system_libs", + build_setting_default = True, +) + +config_setting( + name = "do_inline_system_libs", + flag_values = {":inline_system_libs": "True"}, +) + +# @llvm//:headers carries every LLVM/Clang header root (source + generated) with +# the matching -isystem includes, mirroring CMake's +# include_directories(SYSTEM ${LLVM/CLANG_INCLUDE_DIRS}). + +# utils/TableGen: the cppinterop-tblgen tool. CMake add_tablegen links +# LLVM Support + TableGen; the broad llvm_linkopts() covers those. +cc_binary( + name = "cppinterop-tblgen", + srcs = [ + "utils/TableGen/CppInterOpEmitter.cpp", + "utils/TableGen/TableGen.cpp", + ], + # The static LLVM .a's the -l flags pull must be staged as link inputs. + additional_linker_inputs = ["@llvm//:lib_files"], + copts = CPPINTEROP_COPTS, + data = ["@llvm//:all_files"], + # A standalone tool using llvm::TableGen: needs the static components, not + # just the clang-cpp dylib. + linkopts = llvm_tblgen_linkopts(), + deps = [ + ":llvm_system_libs", + "@llvm//:headers", + ], +) + +# Run cppinterop-tblgen over CppInterOp.td -> the 4 generated .inc headers. +cppinterop_tblgen_inc_files() + +filegroup( + name = "tblgen_inc_files", + srcs = [ + "include/CppInterOp/CXCppInterOpDecl.inc", + "include/CppInterOp/CXCppInterOpImpl.inc", + "include/CppInterOp/CppInterOpAPI.inc", + "include/CppInterOp/CppInterOpDecl.inc", + ], +) + +# configure_file(BuildInfo.inc.in -> include/CppInterOp/BuildInfo.inc). +cppinterop_buildinfo_inc() + +# `c++` shim on PATH for Cpp::DetectSystemCompilerIncludePaths' popen(). +cppinterop_cxx_shim() + +# Public headers + generated .inc, consumed by the library and the tests. +filegroup( + name = "headers", + srcs = glob(["include/**/*.h"]) + [ + "include/CppInterOp/BuildInfo.inc", + ":tblgen_inc_files", + ], +) + +# lib/CppInterOp private headers, needed when compiling the .cpp sources. +filegroup( + name = "internal_headers", + srcs = glob(["lib/CppInterOp/*.h"]), +) + +# lib/CppInterOp: the clangCppInterOp library (CPPINTEROP_USE_REPL backend, +# non-EMSCRIPTEN sources from add_llvm_library(clangCppInterOp ...)). +cc_library( + name = "lib", + srcs = [ + "lib/CppInterOp/CXCppInterOp.cpp", + "lib/CppInterOp/CXCppInterOpGenerated.cpp", + "lib/CppInterOp/CppInterOp.cpp", + "lib/CppInterOp/CppInterOpDispatch.cpp", + "lib/CppInterOp/DynamicLibraryManager.cpp", + "lib/CppInterOp/DynamicLibraryManagerSymbol.cpp", + "lib/CppInterOp/ErrorInternal.cpp", + "lib/CppInterOp/Paths.cpp", + "lib/CppInterOp/Tracing.cpp", + ] + glob(["lib/CppInterOp/*.h"]), + hdrs = [":headers"], + # _CINDEX_LIB_: target_compile_definitions PUBLIC (CINDEX_LINKAGE). + # _GNU_SOURCE: top-level add_definitions(-D_GNU_SOURCE). + copts = CPPINTEROP_COPTS + [ + "-D_CINDEX_LIB_", + "-D_GNU_SOURCE", + ], + data = ["@llvm//:all_files"], + includes = ["include"], + # :llvm_system_libs carries zlib/zstd (host -l flags by default; a hermetic + # consumer swaps in shipped .so's). It rides on :lib so it propagates into + # the solib link below and any direct cc_test linker. + deps = [ + ":llvm_system_libs", + "@llvm//:headers", + ], +) + +# libclangCppInterOp.so. The lib/ prefix in shared_lib_name matters for +# downstream loaders that expect it under lib/. LLVM is linked here (the .so +# boundary) -- not on :lib -- so the flags don't propagate into every +# downstream consumer that only needs to link this .so dynamically. +cc_shared_library( + name = "solib", + additional_linker_inputs = ["@llvm//:lib_files"], + shared_lib_name = "lib/libclangCppInterOp.so", + # Base LLVM link flags + the LTO codegen level selected via :lto_opt_level + # (a no-op on a non-LTO @llvm). Kept separate from llvm_linkopts() so the + # level can vary per consumer without a different llvm_linkopts() per build. + user_link_flags = llvm_linkopts() + select({ + ":lto_opt_O" + lvl: llvm_lto_opt_linkopts(lvl) + for lvl in LTO_OPT_LEVELS + }) + select({ + # Host build: inline the bare -l system libs (cc_shared_library drops a + # cc_library dep's linkopts, so they must be here). Hermetic consumer: + # empty here, ships the libs as .so files via :llvm_system_libs instead. + ":do_inline_system_libs": llvm_system_libs(), + "//conditions:default": [], + }), + deps = [":lib"], +) + +# Everything a downstream consumer needs at runtime. +filegroup( + name = "data", + srcs = [ + ":headers", + ":solib", + "@llvm//:all_files", + ], +) + +# --------------------------------------------------------------------------- +# unittests/CppInterOp (native only; the EMSCRIPTEN branches are skipped). +# --------------------------------------------------------------------------- + +# Tests transitively include clang/llvm headers (via Utils.h / CppInterOp.h); +# the cppinterop_cc_test macro supplies @llvm//:headers (with its -isystem +# roots) automatically, so tests need no per-target LLVM include wiring. + +# TestSharedLib: a helper DSO the unit tests dlopen and the VTableOverlay +# bench links. (add_llvm_library(TestSharedLib SHARED ...)) +cc_library( + name = "test_lib", + srcs = ["unittests/CppInterOp/TestSharedLib/TestSharedLib.cpp"], + hdrs = ["unittests/CppInterOp/TestSharedLib/TestSharedLib.h"], + copts = BASE_COPTS, + includes = ["unittests/CppInterOp"], +) + +cc_shared_library( + name = "test_solib", + shared_lib_name = "TestSharedLib.so", + deps = [":test_lib"], +) + +# TestDownstreamLib: mirrors libcppyy-backend's contract -- uses the inline +# path with its own slot storage and deliberately +# does NOT link clangCppInterOp, so any unsatisfied inline-header UND ref +# trips here. NDEBUG/-O2/-fno-exceptions/-fno-rtti = BASE_COPTS. +cc_library( + name = "test_downstream_lib", + srcs = ["unittests/CppInterOp/TestDownstreamLib/TestDownstreamLib.cpp"], + hdrs = ["unittests/CppInterOp/TestDownstreamLib/TestDownstreamLib.h"] + [":headers"], + # NDEBUG (from BASE_COPTS) drops the JitCall asserts that call + # AreArgumentsValid; this DSO is deliberately NOT linked against + # clangCppInterOp, so leaving the asserts in would make that an undefined + # symbol. Mirrors cppyy's release backend. + copts = BASE_COPTS, + includes = [ + "include", + "unittests/CppInterOp/TestDownstreamLib", + ], +) + +cc_shared_library( + name = "test_downstream_solib", + shared_lib_name = "libTestDownstreamLib.so", + deps = [":test_downstream_lib"], +) + +# TestDownstreamLoader: dlopens TestDownstreamLib (via TEST_DOWNSTREAM_LIB_PATH) +# and takes argv[1] = the clangCppInterOp solib path. Not linked against +# clangCppInterOp; -ldl for dlopen. +cc_test( + name = "TestDownstreamLoader", + srcs = ["unittests/CppInterOp/TestDownstreamLoader.cpp"], + args = ["$(rootpath :solib)"], + copts = BASE_COPTS + [ + "-DTEST_DOWNSTREAM_LIB_PATH=\\\"$(rootpath :test_downstream_solib)\\\"", + ], + data = [ + ":solib", + ":test_downstream_solib", + ], + linkopts = ["-ldl"], +) + +# CAPITestC: pure-C compile check of the generated C API. C-only flags; +# must NOT inherit the C++ copts (GCC's C frontend rejects -fno-rtti etc). +cc_library( + name = "CAPITestC", + srcs = ["unittests/CppInterOp/CAPITestC.c"], + hdrs = [":headers"], + copts = [ + "-Werror", + "-Wall", + "-Wno-unused-command-line-argument", + ], + includes = ["include"], +) + +# CppInterOpTests: the main gtest suite. Source list = CMake's +# add_cppinterop_unittest(CppInterOpTests ...) plus the non-EMSCRIPTEN +# target_sources additions (CUDATest, VTableOverlayTest) plus main.cpp. +cppinterop_cc_test( + name = "CppInterOpTests", + srcs = [ + "unittests/CppInterOp/CAPITest.cpp", + "unittests/CppInterOp/CUDATest.cpp", + "unittests/CppInterOp/CommentReflectionTest.cpp", + "unittests/CppInterOp/CppInterOpThunksTest.cpp", + "unittests/CppInterOp/EnumReflectionTest.cpp", + "unittests/CppInterOp/FunctionReflectionTest.cpp", + "unittests/CppInterOp/HandleTypesTest.cpp", + "unittests/CppInterOp/InterpreterTest.cpp", + "unittests/CppInterOp/JitTest.cpp", + "unittests/CppInterOp/ResultTest.cpp", + "unittests/CppInterOp/ScopeReflectionTest.cpp", + "unittests/CppInterOp/TypeReflectionTest.cpp", + "unittests/CppInterOp/Utils.cpp", + "unittests/CppInterOp/Utils.h", + "unittests/CppInterOp/VTableOverlayTest.cpp", + "unittests/CppInterOp/VariableReflectionTest.cpp", + "unittests/CppInterOp/main.cpp", + ], + # CMake sets -Wno-pedantic on VariableReflectionTest.cpp; applied suite-wide. + extra_copts = ["-Wno-pedantic"], + extra_deps = [":CAPITestC"], +) + +# DynamicLibraryManagerTests: dlopens TestSharedLib at runtime (test_solib is +# already shipped as data by the cppinterop_cc_test macro). +cppinterop_cc_test( + name = "DynamicLibraryManagerTests", + srcs = [ + "unittests/CppInterOp/DynamicLibraryManagerTest.cpp", + "unittests/CppInterOp/Utils.cpp", + "unittests/CppInterOp/Utils.h", + "unittests/CppInterOp/main.cpp", + ], +) + +# TracingTests reads CppInterOp.h, the generated .inc, and lib/*.cpp at runtime, +# relative to CPPINTEROP_DIR / CPPINTEROP_BINARY_DIR (the repo's runfiles root, +# which is the test's cwd). repo_rloc self-corrects between the standalone build +# (cppinterop is the main repo -> ".") and being an external dep (-> "../"). +# This BUILD is always @cppinterop, so is_main_repo() marks the self-reference. +# No test-source changes needed. +_CPPINTEROP_RUNFILES_DIR = repo_rloc( + "@cppinterop", + is_main_repo(repository_name()), +) + +cppinterop_cc_test( + name = "TracingTests", + srcs = [ + "unittests/CppInterOp/TracingTests.cpp", + "unittests/CppInterOp/Utils.cpp", + "unittests/CppInterOp/Utils.h", + "unittests/CppInterOp/main.cpp", + ], + data = glob(["lib/CppInterOp/*.cpp"]), + extra_copts = [ + "-DCPPINTEROP_DIR=\\\"" + _CPPINTEROP_RUNFILES_DIR + "\\\"", + "-DCPPINTEROP_BINARY_DIR=\\\"" + _CPPINTEROP_RUNFILES_DIR + "\\\"", + ], +) + +# DispatchTests: dlopens clangCppInterOp via RTLD_LOCAL; must NOT link it. +cppinterop_dispatch_cc_test( + name = "CppInterOpTestsDispatch", + srcs = [ + "unittests/CppInterOp/DispatchInit.cpp", + "unittests/CppInterOp/DispatchSmokeTest.cpp", + "unittests/CppInterOp/DispatchTest.cpp", + "unittests/CppInterOp/main.cpp", + ], +) + +# Verify the dispatch binary is not linked against libclangCppInterOp -- if +# it is, the dispatch tests are not truly exercising RTLD_LOCAL isolation. +sh_test( + name = "CppInterOpTestsDispatch-isolation", + srcs = ["bazel-support/check_no_clangCppInterOp_link.sh"], + args = ["$(rootpath :CppInterOpTestsDispatch)"], + data = [":CppInterOpTestsDispatch"], +) + +# VTableOverlayCrossTUBench: cross-TU dispatch-cost check; the dispatch loop +# lives in TestSharedLib so the call site can't devirtualize here. +# VTableOverlayCrossTUBench provides its own main() and only uses the +# header-inline TestUtils::BitCastFn, so no main.cpp / Utils.cpp here. +cppinterop_cc_test( + name = "VTableOverlayCrossTUBench", + srcs = [ + "unittests/CppInterOp/PerfCompare.h", + "unittests/CppInterOp/TestSharedLib/TestSharedLib.h", + "unittests/CppInterOp/Utils.h", + "unittests/CppInterOp/VTableOverlayCrossTUBench.cpp", + ], + extra_deps = [ + ":test_lib", + "@google_benchmark//:benchmark", + ], + extra_dynamic_deps = [":test_solib"], +) + +# Cross-repo test_suite can't use wildcards; list every test target. +test_suite( + name = "tests", + tests = [ + ":CppInterOpTests", + ":CppInterOpTestsDispatch", + ":CppInterOpTestsDispatch-isolation", + ":DynamicLibraryManagerTests", + ":TestDownstreamLoader", + ":TracingTests", + ":VTableOverlayCrossTUBench", + ], +) diff --git a/interpreter/CppInterOp/MODULE.bazel b/interpreter/CppInterOp/MODULE.bazel new file mode 100644 index 0000000000000..07e401de3720f --- /dev/null +++ b/interpreter/CppInterOp/MODULE.bazel @@ -0,0 +1,20 @@ +module( + name = "cppinterop", + version = "0.1.0", +) + +bazel_dep(name = "cppyy_bazel", version = "0.1.0") +local_path_override( + module_name = "cppyy_bazel", + path = "../cppyy/bazel", +) + +bazel_dep(name = "bazel_skylib", version = "1.7.1") +bazel_dep(name = "googletest", version = "1.15.2") +bazel_dep(name = "google_benchmark", version = "1.9.1") + +llvm = use_extension("@cppyy_bazel//:llvm.bzl", "llvm") +use_repo(llvm, llvm = "cppjit_llvm") + +# Opt in to the centralized clang toolchain from cppyy_bazel (standalone build). +register_toolchains("@llvm//:cc_toolchain") diff --git a/interpreter/CppInterOp/README.md b/interpreter/CppInterOp/README.md index f9b8aee994ace..70f23da6cce95 100644 --- a/interpreter/CppInterOp/README.md +++ b/interpreter/CppInterOp/README.md @@ -612,6 +612,41 @@ cmake --build . --target check-cppinterop --parallel $env:ncpus +## Building with Bazel (experimental) + +CMake (above) is the supported way to build CppInterOp. An **experimental, +best-effort** Bazel build is also provided. A non-gating CI job exercises it +against an upstream LLVM 22 release (via `compiler-research/ci-workflows`'s +`setup-llvm`), so a failure surfaces as a warning rather than blocking a PR; it +may still lag the CMake build. + +The Bazel build consumes a local LLVM/Clang tree rather than building one (LLVM +20–22 are supported, matching the CMake version range). Point it at one with the +`LLVM_DIR` environment variable -- either an LLVM *build* tree or a release +*install* tree; the build adapts to whichever (static-archive or shared +`libLLVM.so`) layout the tree ships: + +```sh +export LLVM_DIR=/path/to/llvm +``` + +The shared Bazel machinery lives in the `cppyy_bazel` module under the sibling +`cppyy` repository. CppInterOp's `MODULE.bazel` references it via +`local_path_override(... path = "../cppyy/bazel")`, so check out `cppyy` +alongside CppInterOp and run Bazel from the CppInterOp directory: + +```text +/ + CppInterOp/ # this repo + cppyy/ # provides cppyy_bazel +``` + +```sh +cd CppInterOp +LLVM_DIR=/path/to/llvm bazelisk build //... +LLVM_DIR=/path/to/llvm bazelisk test //:tests +``` + ______________________________________________________________________ Further Reading: [C++ Language Interoperability Layer](https://compiler-research.org/libinterop/) diff --git a/interpreter/CppInterOp/bazel-support/check_no_clangCppInterOp_link.sh b/interpreter/CppInterOp/bazel-support/check_no_clangCppInterOp_link.sh new file mode 100644 index 0000000000000..93834ef2c59a0 --- /dev/null +++ b/interpreter/CppInterOp/bazel-support/check_no_clangCppInterOp_link.sh @@ -0,0 +1,4 @@ +#!/bin/bash +# Fail if $1 links libclangCppInterOp: the dispatch tests must reach it only +# via dlopen(RTLD_LOCAL), proving symbol isolation. +ldd "$1" | grep -q libclangCppInterOp && exit 1 || exit 0 diff --git a/interpreter/CppInterOp/cmake/modules/GoogleBenchmark.cmake b/interpreter/CppInterOp/cmake/modules/GoogleBenchmark.cmake new file mode 100644 index 0000000000000..6776cd496677e --- /dev/null +++ b/interpreter/CppInterOp/cmake/modules/GoogleBenchmark.cmake @@ -0,0 +1,73 @@ +# Build Google Benchmark as an ExternalProject, mirroring GoogleTest.cmake, +# so the perf-as-validity tests get the library without a system dependency. +# BUILD_BYPRODUCTS, the sub-build's binary dir (ExternalProject's default), +# and IMPORTED_LOCATION (resolved via ExternalProject_Get_Property binary_dir) +# must agree on where benchmark builds; tracking CMAKE_CURRENT_BINARY_DIR +# keeps them in sync. +set(_benchmark_byproduct_binary_dir + ${CMAKE_CURRENT_BINARY_DIR}/googlebenchmark-prefix/src/googlebenchmark-build) +set(_benchmark_byproducts + ${_benchmark_byproduct_binary_dir}/src/${CMAKE_STATIC_LIBRARY_PREFIX}benchmark${CMAKE_STATIC_LIBRARY_SUFFIX} + ) + +if(APPLE) + set(EXTRA_BENCHMARK_OPTS -DCMAKE_OSX_SYSROOT=${CMAKE_OSX_SYSROOT}) +endif() + +include(ExternalProject) +# Forward parent CMAKE_CXX_FLAGS to the sub-build so sanitizer and +# -stdlib=libc++ additions don't get dropped (else benchmark builds against +# system defaults and ABI-clashes with the parent at link). Benchmark is +# third-party and not warning-clean under LLVM's -Werror/-pedantic regime +# (e.g. clang 22's -Wc2y-extensions fires on the library's __COUNTER__ use), +# so a trailing -w keeps a new upstream/compiler warning from turning the +# dependency build red. +set(GOOGLEBENCHMARK_CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") +if(MSVC) + set(GOOGLEBENCHMARK_CMAKE_CXX_FLAGS "${GOOGLEBENCHMARK_CMAKE_CXX_FLAGS} /w") +else() + set(GOOGLEBENCHMARK_CMAKE_CXX_FLAGS "${GOOGLEBENCHMARK_CMAKE_CXX_FLAGS} -w") +endif() + +ExternalProject_Add( + googlebenchmark + GIT_REPOSITORY https://github.com/google/benchmark.git + GIT_SHALLOW FALSE + GIT_TAG v1.9.5 + UPDATE_COMMAND "" + CMAKE_ARGS -DCMAKE_BUILD_TYPE=$ + -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER} + -DCMAKE_C_FLAGS=${CMAKE_C_FLAGS} + -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER} + -DCMAKE_CXX_FLAGS=${GOOGLEBENCHMARK_CMAKE_CXX_FLAGS} + -DCMAKE_EXE_LINKER_FLAGS=${CMAKE_EXE_LINKER_FLAGS} + -DCMAKE_SHARED_LINKER_FLAGS=${CMAKE_SHARED_LINKER_FLAGS} + -DCMAKE_AR=${CMAKE_AR} + # Benchmark's own tests need a separate gtest checkout and turn + # its build red under -Werror on newer compilers; we only want + # the library. + -DBENCHMARK_ENABLE_TESTING=OFF + -DBENCHMARK_ENABLE_GTEST_TESTS=OFF + -DBENCHMARK_ENABLE_WERROR=OFF + -DBENCHMARK_ENABLE_INSTALL=OFF + ${EXTRA_BENCHMARK_OPTS} + BUILD_COMMAND ${CMAKE_COMMAND} --build ${_benchmark_byproduct_binary_dir}/ --config $ + INSTALL_COMMAND "" + BUILD_BYPRODUCTS ${_benchmark_byproducts} + LOG_DOWNLOAD ON + LOG_CONFIGURE ON + LOG_BUILD ON + TIMEOUT 600 + ) + +ExternalProject_Get_Property(googlebenchmark source_dir) +set(BENCHMARK_INCLUDE_DIR ${source_dir}/include) +# Prevents bug https://gitlab.kitware.com/cmake/cmake/issues/15052 +file(MAKE_DIRECTORY ${BENCHMARK_INCLUDE_DIR}) + +add_library(benchmark IMPORTED STATIC GLOBAL) +set_target_properties(benchmark PROPERTIES + IMPORTED_LOCATION "${_benchmark_byproducts}" + INTERFACE_INCLUDE_DIRECTORIES "${BENCHMARK_INCLUDE_DIR}" + ) +add_dependencies(benchmark googlebenchmark) diff --git a/interpreter/CppInterOp/cmake/modules/GoogleTest.cmake b/interpreter/CppInterOp/cmake/modules/GoogleTest.cmake index eb7f73f66dd49..91f56e7b907fe 100644 --- a/interpreter/CppInterOp/cmake/modules/GoogleTest.cmake +++ b/interpreter/CppInterOp/cmake/modules/GoogleTest.cmake @@ -1,4 +1,5 @@ -# BUILD_BYPRODUCTS, the explicit -B in CONFIGURE_COMMAND, and +# BUILD_BYPRODUCTS, the sub-build's binary dir (ExternalProject's +# default, passed as -B explicitly on the emscripten path), and # IMPORTED_LOCATION (resolved via ExternalProject_Get_Property # binary_dir) all need to agree on where googletest builds. # ExternalProject's binary_dir defaults to @@ -43,37 +44,41 @@ if (EMSCRIPTEN) set(build_cmd emmake${EMCC_SUFFIX} make) endif() else() - set(config_cmd ${CMAKE_COMMAND}) set(build_cmd ${CMAKE_COMMAND} --build ${CMAKE_CURRENT_BINARY_DIR}/googletest-prefix/src/googletest-build/ --config $) endif() +set(_gtest_cmake_args + -DCMAKE_BUILD_TYPE=$ + -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER} + -DCMAKE_C_FLAGS=${CMAKE_C_FLAGS} + -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER} + -DCMAKE_CXX_FLAGS=${GOOGLETEST_CMAKE_CXX_FLAGS} + # HandleLLVMOptions puts -stdlib=libc++ / -fsanitize=* + # in CMAKE_*_LINKER_FLAGS for LLVM_USE_SANITIZER. + -DCMAKE_EXE_LINKER_FLAGS=${CMAKE_EXE_LINKER_FLAGS} + -DCMAKE_MODULE_LINKER_FLAGS=${CMAKE_MODULE_LINKER_FLAGS} + -DCMAKE_SHARED_LINKER_FLAGS=${CMAKE_SHARED_LINKER_FLAGS} + -DCMAKE_AR=${CMAKE_AR} + -DCMAKE_INSTALL_PREFIX=${CMAKE_INSTALL_PREFIX} + ${EXTRA_GTEST_OPTS}) + +if(EMSCRIPTEN) + set(_gtest_configure + CONFIGURE_COMMAND ${config_cmd} -G ${CMAKE_GENERATOR} + -S ${CMAKE_CURRENT_BINARY_DIR}/googletest-prefix/src/googletest/ + -B ${CMAKE_CURRENT_BINARY_DIR}/googletest-prefix/src/googletest-build/ + ${_gtest_cmake_args}) +else() + set(_gtest_configure CMAKE_ARGS ${_gtest_cmake_args}) +endif() + ExternalProject_Add( googletest GIT_REPOSITORY https://github.com/google/googletest.git GIT_SHALLOW FALSE GIT_TAG fa8438ae6b70c57010177de47a9f13d7041a6328 UPDATE_COMMAND "" - # # Force separate output paths for debug and release builds to allow easy - # # identification of correct lib in subsequent TARGET_LINK_LIBRARIES commands - # CMAKE_ARGS -DCMAKE_ARCHIVE_OUTPUT_DIRECTORY_DEBUG:PATH=DebugLibs - # -DCMAKE_ARCHIVE_OUTPUT_DIRECTORY_RELEASE:PATH=ReleaseLibs - # -Dgtest_force_shared_crt=ON - CONFIGURE_COMMAND ${config_cmd} -G ${CMAKE_GENERATOR} - -S ${CMAKE_CURRENT_BINARY_DIR}/googletest-prefix/src/googletest/ - -B ${CMAKE_CURRENT_BINARY_DIR}/googletest-prefix/src/googletest-build/ - -DCMAKE_BUILD_TYPE=$ - -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER} - -DCMAKE_C_FLAGS=${CMAKE_C_FLAGS} - -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER} - -DCMAKE_CXX_FLAGS=${GOOGLETEST_CMAKE_CXX_FLAGS} - # HandleLLVMOptions puts -stdlib=libc++ / -fsanitize=* - # in CMAKE_*_LINKER_FLAGS for LLVM_USE_SANITIZER. - -DCMAKE_EXE_LINKER_FLAGS=${CMAKE_EXE_LINKER_FLAGS} - -DCMAKE_MODULE_LINKER_FLAGS=${CMAKE_MODULE_LINKER_FLAGS} - -DCMAKE_SHARED_LINKER_FLAGS=${CMAKE_SHARED_LINKER_FLAGS} - -DCMAKE_AR=${CMAKE_AR} - -DCMAKE_INSTALL_PREFIX=${CMAKE_INSTALL_PREFIX} - ${EXTRA_GTEST_OPTS} + ${_gtest_configure} BUILD_COMMAND ${build_cmd} # Disable install step INSTALL_COMMAND "" diff --git a/interpreter/CppInterOp/cppinterop-version.tag b/interpreter/CppInterOp/cppinterop-version.tag index 93e00085c4566..dce1aa01f73ce 100644 --- a/interpreter/CppInterOp/cppinterop-version.tag +++ b/interpreter/CppInterOp/cppinterop-version.tag @@ -1 +1 @@ -ed3696d403c4064c494375087eb92e612cfd7245 +794f495d9e8210247b7accb068f7c6c1dc184231 diff --git a/interpreter/CppInterOp/include/CppInterOp/Box.h b/interpreter/CppInterOp/include/CppInterOp/Box.h new file mode 100644 index 0000000000000..d28ee4f58f294 --- /dev/null +++ b/interpreter/CppInterOp/include/CppInterOp/Box.h @@ -0,0 +1,291 @@ +//===--- Box.h - Typed result carrier across the AOT/JIT boundary -*- C++ -*-=// +// +// Part of the compiler-research project, under the Apache License v2.0 with +// LLVM Exceptions. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +// +// Cpp::Box is the typed result vocabulary that crosses the binding-AOT / +// clang-repl-JIT boundary in both directions: catalog thunks Create a +// Box to hand to the runtime, Evaluate returns one to hand back. Storage +// is hybrid -- fundamentals (K_Bool ... K_LongDouble) live inline in the +// union; K_PtrOrObj keeps an owned pointer plus a type-erased +// {retain, release} pair so the public header sees only `void*` and +// function pointers, never the concrete payload type. Copy is shallow + +// refcounted on K_PtrOrObj (matching clang::Value's model); the bridge +// composes only when both layers share that semantic. +// +// The AOT path `Box::Create(x).visit(v)` is designed to fold to a +// direct `v(x)` -- the always_inline annotations below preserve this +// against compiler inliner heuristics (validated separately on clang 17 +// and gcc 12 -O3; this commit does not pin the property in-tree). +// +// CPP_BOX_BUILTIN_TYPES is the single source of truth: enum positions, +// storage union slots, KindOf specialisations, and visit's switch are +// all generated from this one X-macro -- one edit per new fundamental. +// Mirrors clang's REPL_BUILTIN_TYPES so the Evaluate bridge in +// CppInterOp.cpp is a Kind-to-Kind switch with no translation table. +// +//===----------------------------------------------------------------------===// + +#ifndef CPPINTEROP_BOX_H +#define CPPINTEROP_BOX_H + +// Box.h is meant to be cheap to parse: the JIT pulls it whenever a +// catalog thunk or evaluated snippet references Cpp::Box, so every +// pulled-in standard header is paid at interpretation time. We avoid +// CppInterOpTypes.h (which drags in / / ) and +// keep behind NDEBUG. Under NDEBUG, this file pulls in zero +// standard or project headers. + +#ifndef NDEBUG +#include +#endif + +// Cross-platform always_inline. Duplicated from CppInterOpTypes.h's +// CPPINTEROP_ALWAYS_INLINE under a guard so consumers don't have to +// pull that (heavier) header just to use Box. +#ifndef CPPINTEROP_ALWAYS_INLINE +#if defined(_MSC_VER) +#define CPPINTEROP_ALWAYS_INLINE __forceinline +#elif defined(__GNUC__) || defined(__clang__) +#define CPPINTEROP_ALWAYS_INLINE __attribute__((always_inline)) inline +#else +#define CPPINTEROP_ALWAYS_INLINE inline +#endif +#endif + +// Cross-platform unreachable hint used in visit's default arms. +#ifndef CPPINTEROP_UNREACHABLE +#if defined(_MSC_VER) +#define CPPINTEROP_UNREACHABLE() __assume(0) +#elif defined(__GNUC__) || defined(__clang__) +#define CPPINTEROP_UNREACHABLE() __builtin_unreachable() +#else +#define CPPINTEROP_UNREACHABLE() ((void)0) +#endif +#endif + +namespace Cpp { + +// FIXME: clang's REPL_BUILTIN_TYPES lists `unsigned char` twice (Char_U + +// UChar) to disambiguate the platform-default signedness of `char` from an +// explicit `signed char` / `unsigned char`. The proper KindOf() +// resolution uses std::is_signed_v to pick K_Char_S vs K_Char_U; +// the duplicate Char_U entry is reached only via the runtime bridge +// (Evaluate). We keep this table single-valued per C++ type for unambiguous +// per-T specializations; Evaluate adds the K_Char_U case explicitly. +#define CPP_BOX_BUILTIN_TYPES \ + X(bool, Bool) \ + X(char, Char_S) \ + X(signed char, SChar) \ + X(unsigned char, UChar) \ + X(short, Short) \ + X(unsigned short, UShort) \ + X(int, Int) \ + X(unsigned int, UInt) \ + X(long, Long) \ + X(unsigned long, ULong) \ + X(long long, LongLong) \ + X(unsigned long long, ULongLong) \ + X(float, Float) \ + X(double, Double) \ + X(long double, LongDouble) + +class Box { +public: + // `int` (not `unsigned char`) to work around compiler-research/cppyy#223. + enum Kind : int { +#define X(type, name) K_##name, + CPP_BOX_BUILTIN_TYPES +#undef X + K_Char_U, // alias of UChar storage; reached only by runtime bridge + K_Void, + K_PtrOrObj, + K_Unspecified, + }; + + /// Operations vtable for a K_PtrOrObj payload. Defined once per concrete + /// payload type in the TU that knows the type (see Evaluate's + /// kCompatValueOps in CppInterOp.cpp). The Box stores a pointer to this + /// const-static struct; the storage slot is 2 pointers (16 bytes). + /// + /// retain/release implement intrusive ref-counting: AdoptObject installs + /// the payload with refcount 1; copy ctor / copy assign call retain; + /// destructor calls release; the producer's release fires the payload's + /// destructor when the last ref drops. + struct ObjectOps { + /// Increment the payload's refcount. Must be noexcept; called from the + /// (noexcept) copy ctor / copy assign. + void (*retain)(void*) noexcept; + /// Decrement the payload's refcount; on the last drop, run the payload's + /// destructor and free storage. Must be noexcept; called from ~Box. + void (*release)(void*) noexcept; + }; + +private: + union Storage { +#define X(type, name) type m_##name; + CPP_BOX_BUILTIN_TYPES +#undef X + struct { + void* m_Ptr; + const ObjectOps* m_Ops; + } m_Object; + }; + + Kind m_kind = K_Unspecified; + void* m_Type = nullptr; + Storage m_storage = {}; + + template static constexpr Kind KindOf() noexcept; + template static T& slot(Storage& s) noexcept; + template static const T& slot(const Storage& s) noexcept; + +public: + // -- rule of five: refcount-shared on K_PtrOrObj, bitwise on fundamentals -- + Box() = default; + Box(const Box& o) noexcept + : m_kind(o.m_kind), m_Type(o.m_Type), m_storage(o.m_storage) { + // Kind check first so fundamentals constant-fold the branch away + // (AOT-fold preserved). K_PtrOrObj bumps the payload refcount. + if (m_kind == K_PtrOrObj && m_storage.m_Object.m_Ops) + m_storage.m_Object.m_Ops->retain(m_storage.m_Object.m_Ptr); + } + Box& operator=(const Box& o) noexcept { + if (this != &o) { + this->~Box(); + m_kind = o.m_kind; + m_Type = o.m_Type; + m_storage = o.m_storage; + if (m_kind == K_PtrOrObj && m_storage.m_Object.m_Ops) + m_storage.m_Object.m_Ops->retain(m_storage.m_Object.m_Ptr); + } + return *this; + } + Box(Box&& o) noexcept + : m_kind(o.m_kind), m_Type(o.m_Type), m_storage(o.m_storage) { + // moved-from: dtor sees K_Unspecified → no release of the stolen ref. + o.m_kind = K_Unspecified; + } + Box& operator=(Box&& o) noexcept { + if (this != &o) { + this->~Box(); + m_kind = o.m_kind; + m_Type = o.m_Type; + m_storage = o.m_storage; + o.m_kind = K_Unspecified; + } + return *this; + } + // always_inline so a static Kind constant-propagates into the dtor + // body and the K_PtrOrObj branch dead-strips on the AOT path. + CPPINTEROP_ALWAYS_INLINE ~Box() noexcept { + if (m_kind == K_PtrOrObj && m_storage.m_Object.m_Ops) + m_storage.m_Object.m_Ops->release(m_storage.m_Object.m_Ptr); + } + + Kind getKind() const noexcept { return m_kind; } + void* getType() const noexcept { return m_Type; } + + /// AOT-typed extraction. \c T must match the runtime \c Kind exactly + /// (UB to read an inactive union member otherwise -- e.g. calling + /// \c unbox() on a \c K_Long Box). For unknown-kind extraction + /// use \c visit() or check \c getKind() against \c KindOf() first. + /// The assert is a no-op under \c NDEBUG so the AOT-fold path stays + /// zero-overhead. + template T unbox() const noexcept { +#ifndef NDEBUG + assert(m_kind == KindOf() && + "Cpp::Box::unbox(): T does not match the runtime Kind"); +#endif + return slot(m_storage); + } + + /// AOT-typed construction for fundamentals. Sets Kind = KindOf(), + /// stores x. Optional QualType preserves typedef sugar (int8_t vs + /// signed char) the Kind enum collapses. + // always_inline so `Create(x).visit(toPy)` folds bit-identical to + // a direct `toPy(x)` on gcc-12 (default inliner gives up otherwise). + template + CPPINTEROP_ALWAYS_INLINE static Box Create(T x, + void* type = nullptr) noexcept { + Box v; + v.m_kind = KindOf(); + v.m_Type = type; + slot(v.m_storage) = x; + return v; + } + + /// Object-payload construction. `obj` enters with refcount 1 (the + /// producer's invariant); ~Box calls `ops->release(obj)` which on the + /// last drop runs the payload's destructor. The ops table is const-static, + /// defined in the TU that knows the concrete payload type. + static Box AdoptObject(void* obj, const ObjectOps* ops, void* type) noexcept { + Box v; + v.m_kind = K_PtrOrObj; + v.m_Type = type; + v.m_storage.m_Object.m_Ptr = obj; + v.m_storage.m_Object.m_Ops = ops; + return v; + } + + /// Object payload accessor; only valid for K_PtrOrObj. Returns the + /// raw pointer set at AdoptObject time. Layout is producer-defined -- + /// the producer that installed the ObjectOps knows how to extract the + /// underlying object. + void* getObjectPtr() const noexcept { + return m_kind == K_PtrOrObj ? m_storage.m_Object.m_Ptr : nullptr; + } + + /// Runtime convert across Kinds: dispatches on the actual Kind and + /// returns the value reinterpreted (via \c static_cast) as \c T. + /// Mirrors clang::Value::convertTo. Only valid for fundamental + /// Kinds (K_Bool ... K_LongDouble); K_PtrOrObj / K_Void / + /// K_Unspecified are caller-must-check-Kind cases. + template T convertTo() const noexcept { + return visit([](auto x) -> T { return static_cast(x); }); + } + + /// Runtime-typed dispatch via visitor. Switch over Kind, call visitor + /// with the typed T extracted from storage. K_PtrOrObj / K_Void / K_* + /// non-fundamental kinds are not dispatched -- caller checks Kind first. + // always_inline so the switch reduces to the single case the AOT Kind + // resolves to; the other arms then dead-strip. + template + CPPINTEROP_ALWAYS_INLINE auto visit(V&& vis) const -> decltype(vis(int{})) { + switch (m_kind) { +#define X(type, name) \ + case K_##name: \ + return vis(unbox()); + CPP_BOX_BUILTIN_TYPES +#undef X + case K_Char_U: + case K_Void: + case K_PtrOrObj: + case K_Unspecified: + CPPINTEROP_UNREACHABLE(); + } + CPPINTEROP_UNREACHABLE(); + } +}; + +// --- Per-T specializations, generated via the same X-macro ------------------- + +#define X(type, name) \ + template <> constexpr Box::Kind Box::KindOf() noexcept { \ + return K_##name; \ + } \ + template <> inline type& Box::slot(Storage & s) noexcept { \ + return s.m_##name; \ + } \ + template <> inline const type& Box::slot(const Storage& s) noexcept { \ + return s.m_##name; \ + } +CPP_BOX_BUILTIN_TYPES +#undef X + +} // namespace Cpp + +#endif // CPPINTEROP_BOX_H diff --git a/interpreter/CppInterOp/include/CppInterOp/CXCppInterOp.h b/interpreter/CppInterOp/include/CppInterOp/CXCppInterOp.h new file mode 100644 index 0000000000000..95f025f53dbf7 --- /dev/null +++ b/interpreter/CppInterOp/include/CppInterOp/CXCppInterOp.h @@ -0,0 +1,71 @@ +//===- CXCppInterOp.h - C API for the CppInterOp library --------*- C -*-===// +// +// Part of the compiler-research project, under the Apache License v2.0 with +// LLVM Exceptions. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +// +// Public C API for CppInterOp. Wraps the tablegen-generated declarations +// in CXCppInterOpDecl.inc and adds the hand-written extras that cannot +// be emitted by the generator (NoCWrapper return types, mixed return + +// out-param shapes). +// +//===----------------------------------------------------------------------===// + +#ifndef CPPINTEROP_CXCPPINTEROP_H +#define CPPINTEROP_CXCPPINTEROP_H + +#include "CppInterOp/CppInterOpTypes.h" + +#ifdef __cplusplus +// The generated .inc spells C-API parameter and return types with the +// prefixed C-side names (CppDeclRef etc.). For C++ TUs that opt into the +// C API by including this header, alias each prefixed name to its +// namespaced Cpp::* counterpart — they're layout-identical, so this +// makes them the same C++ type and no conversion happens at call sites. +// C++ TUs that include only CppInterOp.h (the C++ API header) never see +// these aliases — the prefixed spelling stays out of their namespace. +using CppDeclRef = Cpp::DeclRef; +using CppTypeRef = Cpp::TypeRef; +using CppFuncRef = Cpp::FuncRef; +using CppObjectRef = Cpp::ObjectRef; +using CppInterpRef = Cpp::InterpRef; +using CppConstDeclRef = Cpp::ConstDeclRef; +using CppConstTypeRef = Cpp::ConstTypeRef; +using CppConstFuncRef = Cpp::ConstFuncRef; + +// The C-linkage functions return Cpp::*Ref types (one-word PODs with +// inline constructors). Clang warns about returning a "user-defined +// type" with C linkage even though the ABI is fine — the C-side +// spelling is a layout-identical struct without constructors, and +// HandleTypesTest::AbiCompatibleWithVoidPtr proves the call convention +// matches. Silence the warning for downstream C++ consumers using -I. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wreturn-type-c-linkage" +extern "C" { +#endif + +#include "CppInterOp/CXCppInterOpDecl.inc" + +// --- Hand-written wrappers (see lib/CppInterOp/CXCppInterOp.cpp) --- + +/// C-ABI overload of Cpp::Evaluate. Returns the execution result as +/// a raw \c intptr_t bit pattern; on parse error or no-value-after- +/// success writes \c true to \c *HadError (if non-null) and returns +/// \c ~0UL. The Box-returning C++ overload is marked NoCWrapper. +CPPINTEROP_API intptr_t cppinterop_Evaluate(const char* code, bool* HadError); + +/// Returns the templated method scopes inside \c parent matching +/// \c name. The bool-return-with-vector-outparam shape is not +/// expressible by the tablegen wrapper emitter; callers check +/// \c arr.size > 0 to detect "no matches". +CPPINTEROP_API CppInterOpArray +cppinterop_GetClassTemplatedMethods(const char* name, CppConstDeclRef parent); + +#ifdef __cplusplus +} // extern "C" +#pragma clang diagnostic pop +#endif + +#endif // CPPINTEROP_CXCPPINTEROP_H diff --git a/interpreter/CppInterOp/include/CppInterOp/CppInterOp.h b/interpreter/CppInterOp/include/CppInterOp/CppInterOp.h index 0928cb29f25df..62f99020f159d 100644 --- a/interpreter/CppInterOp/include/CppInterOp/CppInterOp.h +++ b/interpreter/CppInterOp/include/CppInterOp/CppInterOp.h @@ -19,15 +19,88 @@ #endif #include "CppInterOp/CppInterOpTypes.h" +// Cpp::Box must be visible before the tablegen-generated declarations +// below, since Evaluate (and future Value-returning APIs) reference it. +#include "CppInterOp/Box.h" -namespace CppImpl { +#include +#include +#include +#include + +namespace Cpp { // Public API function declarations. Generated by cppinterop-tblgen // from CppInterOp.td. Do not edit this section by hand. #include "CppInterOp/CppInterOpDecl.inc" -} // namespace CppImpl +namespace detail { +// Total slots before the published vtable's address point in an overlay +// block: the ABI prefix of the underlying class (1 on MSVC -- +// complete-object-locator; 2 on Itanium -- offset-to-top + type_info) +// plus 1 hidden slot CppInterOp interposes for the dtor-hook +// self-pointer. Exposed only so the inline VTableOverlayExtraSlot helper +// below can compile the offset; users should not read or write any slots +// directly. +// static (not inline) so the reproducer-as-TU compiles under C++14; +// inline-variable is a C++17 extension and the JIT-compiled reproducer +// is fed back through the interpreter at its default language standard. +#ifdef _WIN32 +static constexpr int kVTableOverlayPrefixSize = 1 + 1; +#else +static constexpr int kVTableOverlayPrefixSize = 2 + 1; +#endif +} // namespace detail + +/// Address of the i-th extra-prefix slot of an instance with an overlay +/// installed via MakeVTableOverlay(..., n_extra_prefix_slots = N, ...). +/// Returns a void*& so callers read and write through the same expression; +/// this is the canonical access path -- bindings should not synthesize the +/// vptr arithmetic themselves. Behavior is undefined if \c inst has no +/// overlay or i >= N. +inline void*& VTableOverlayExtraSlot(void* inst, std::size_t i) { + void** vptr = *reinterpret_cast(inst); + return vptr[-(detail::kVTableOverlayPrefixSize + 1 + static_cast(i))]; +} + +struct VTableOverlayDeleter { + void operator()(VTableOverlay* o) const noexcept { DestroyVTableOverlay(o); } +}; +using UniqueVTableOverlay = + std::unique_ptr; + +/// Owning wrapper over MakeVTableOverlay: each pair maps a virtual method +/// of polymorphic class \c base to its replacement function pointer. The +/// overlay is installed on construction and the original vptr restored when +/// the handle is destroyed. The caller works in reflected methods, not +/// ABI-specific slot indices -- the common path for language bindings. +/// \c n_extra_prefix_slots reserves nullptr-initialized slots immediately +/// before the ABI prefix for the caller to write per-instance data into. +/// \c on_destroy, when non-null, is invoked once after the C++ destructor +/// of \c inst runs (operator-delete path). It receives the original \c +/// inst pointer (which may be dangling at that point -- do not deref) and +/// \c cleanup_data verbatim; bindings use it to release any per-instance +/// state (Python handles, closures) tied to the object's lifetime. The +/// callback must not throw: it runs inside a C++ destructor, so an +/// escaping exception is undefined behavior. +inline UniqueVTableOverlay MakeUniqueVTableOverlay( + void* inst, DeclRef base, + std::initializer_list> overrides, + std::size_t n_extra_prefix_slots = 0, + VTableOverlayDtorHook on_destroy = nullptr, void* cleanup_data = nullptr) { + std::vector methods; + std::vector fns; + methods.reserve(overrides.size()); + fns.reserve(overrides.size()); + for (const auto& p : overrides) { + methods.push_back(p.first); + fns.push_back(p.second); + } + return UniqueVTableOverlay{MakeVTableOverlay( + inst, base, methods.data(), fns.data(), methods.size(), + n_extra_prefix_slots, on_destroy, cleanup_data)}; +} + +} // namespace Cpp -// NOLINTNEXTLINE(misc-unused-alias-decls) -namespace Cpp = CppImpl; #endif // CPPINTEROP_CPPINTEROP_H diff --git a/interpreter/CppInterOp/include/CppInterOp/CppInterOpThunks.h b/interpreter/CppInterOp/include/CppInterOp/CppInterOpThunks.h new file mode 100644 index 0000000000000..7506cc8c17164 --- /dev/null +++ b/interpreter/CppInterOp/include/CppInterOp/CppInterOpThunks.h @@ -0,0 +1,53 @@ +//===--- CppInterOpThunks.h - Variadic dispatcher template ------*- C++ -*-===// +// +// Part of the compiler-research project, under the Apache License v2.0 with +// LLVM Exceptions. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +// +// Header-only variadic-template dispatcher that bindings instantiate per +// signature × slot to populate a precompiled thunk catalog. CppInterOp ships +// the template body; the binding contributes the per-instance handler lookup +// and the language-specific marshaling. +// +// The binding provides a HandlerTraits struct of this shape: +// +// struct HandlerTraits { +// using Handler = /* binding-specific per-instance state */; +// +// // Per-instance lookup -- typically reads the handler pointer from an +// // extra prefix slot reserved via MakeVTableOverlay's +// // n_extra_prefix_slots, i.e. vptr[-(kVTableOverlayPrefixSize + 1)]. +// static Handler& From(void* self); +// +// // Marshal Args... into the target language, call the bound callable +// // at index Slot, and marshal the result back as R. +// template +// static R Call(Handler&, std::size_t Slot, Args... args); +// }; +// +// Each instantiation of dispatch produces a concrete +// function with the right calling convention to install in a patched vtable +// slot. The catalog is a per-binding table of such instantiations indexed by +// (signature shape, slot). +// +//===----------------------------------------------------------------------===// + +#ifndef CPPINTEROP_CPPINTEROPTHUNKS_H +#define CPPINTEROP_CPPINTEROPTHUNKS_H + +#include + +namespace Cpp { +namespace Thunks { + +template +R dispatch(void* self, Args... args) { + return T::template Call(T::From(self), Slot, args...); +} + +} // namespace Thunks +} // namespace Cpp + +#endif // CPPINTEROP_CPPINTEROPTHUNKS_H diff --git a/interpreter/CppInterOp/include/CppInterOp/CppInterOpTypes.h b/interpreter/CppInterOp/include/CppInterOp/CppInterOpTypes.h index 2e7151c05c843..e7332eecd3854 100644 --- a/interpreter/CppInterOp/include/CppInterOp/CppInterOpTypes.h +++ b/interpreter/CppInterOp/include/CppInterOp/CppInterOpTypes.h @@ -42,6 +42,152 @@ #include #include +/// Opaque handle types for the CppInterOp C and C++ API. +/// +/// Each handle wraps a single void* and is a distinct type so the +/// compiler rejects cross-kind misuse (e.g. passing a TypeRef where a +/// DeclRef is expected). Layout is identical across all handles — a +/// single pointer — so they are trivially copyable and ABI-stable. +/// +/// The C and C++ views are declared independently: +/// * C sees `struct CppDeclRef { void* data; }` etc. at global scope, +/// prefixed so they don't collide with generic names from other +/// C libraries. +/// * C++ sees `Cpp::DeclRef` etc. in namespace `Cpp`, no prefix — +/// the namespace already disambiguates. +/// Both have identical layout, so a C-linkage function exchanging these +/// structs across the boundary is byte-identical regardless of which +/// view names it. The TableGen-emitted C-API .inc adds C++ typedef +/// aliases (`using CppDeclRef = Cpp::DeclRef;`) so the generated C +/// wrappers compile under C++ using the prefixed C-side names. + +#ifndef __cplusplus + +typedef struct CppDeclRef { + void* data; +} CppDeclRef; +typedef struct CppTypeRef { + void* data; +} CppTypeRef; +typedef struct CppFuncRef { + void* data; +} CppFuncRef; +typedef struct CppObjectRef { + void* data; +} CppObjectRef; +typedef struct CppInterpRef { + void* data; +} CppInterpRef; +typedef struct CppConstDeclRef { + const void* data; +} CppConstDeclRef; +typedef struct CppConstTypeRef { + const void* data; +} CppConstTypeRef; +typedef struct CppConstFuncRef { + const void* data; +} CppConstFuncRef; + +#else // __cplusplus + +namespace Cpp { + +struct DeclRef { + void* data; + DeclRef() : data(nullptr) {} + DeclRef(void* P) : data(P) {} + DeclRef(decltype(nullptr)) : data(nullptr) {} + explicit operator bool() const { return data != nullptr; } + friend bool operator==(DeclRef a, DeclRef b) { return a.data == b.data; } + friend bool operator!=(DeclRef a, DeclRef b) { return !(a == b); } +}; + +struct TypeRef { + void* data; + TypeRef() : data(nullptr) {} + TypeRef(void* P) : data(P) {} + TypeRef(decltype(nullptr)) : data(nullptr) {} + explicit operator bool() const { return data != nullptr; } + friend bool operator==(TypeRef a, TypeRef b) { return a.data == b.data; } + friend bool operator!=(TypeRef a, TypeRef b) { return !(a == b); } +}; + +struct FuncRef { + void* data; + FuncRef() : data(nullptr) {} + FuncRef(void* P) : data(P) {} + FuncRef(decltype(nullptr)) : data(nullptr) {} + explicit operator bool() const { return data != nullptr; } + friend bool operator==(FuncRef a, FuncRef b) { return a.data == b.data; } + friend bool operator!=(FuncRef a, FuncRef b) { return !(a == b); } +}; + +struct ObjectRef { + void* data; + ObjectRef() : data(nullptr) {} + ObjectRef(void* P) : data(P) {} + ObjectRef(decltype(nullptr)) : data(nullptr) {} + explicit operator bool() const { return data != nullptr; } + friend bool operator==(ObjectRef a, ObjectRef b) { return a.data == b.data; } + friend bool operator!=(ObjectRef a, ObjectRef b) { return !(a == b); } +}; + +struct InterpRef { + void* data; + InterpRef() : data(nullptr) {} + InterpRef(void* P) : data(P) {} + InterpRef(decltype(nullptr)) : data(nullptr) {} + explicit operator bool() const { return data != nullptr; } + friend bool operator==(InterpRef a, InterpRef b) { return a.data == b.data; } + friend bool operator!=(InterpRef a, InterpRef b) { return !(a == b); } +}; + +/// Const handle variants — read-only access to the underlying AST node. +/// Implicit widening from mutable to const is allowed; narrowing is not. + +struct ConstDeclRef { + const void* data; + ConstDeclRef() : data(nullptr) {} + ConstDeclRef(const void* P) : data(P) {} + ConstDeclRef(DeclRef d) : data(d.data) {} + ConstDeclRef(decltype(nullptr)) : data(nullptr) {} + explicit operator bool() const { return data != nullptr; } + friend bool operator==(ConstDeclRef a, ConstDeclRef b) { + return a.data == b.data; + } + friend bool operator!=(ConstDeclRef a, ConstDeclRef b) { return !(a == b); } +}; + +struct ConstTypeRef { + const void* data; + ConstTypeRef() : data(nullptr) {} + ConstTypeRef(const void* P) : data(P) {} + ConstTypeRef(TypeRef d) : data(d.data) {} + ConstTypeRef(decltype(nullptr)) : data(nullptr) {} + explicit operator bool() const { return data != nullptr; } + friend bool operator==(ConstTypeRef a, ConstTypeRef b) { + return a.data == b.data; + } + friend bool operator!=(ConstTypeRef a, ConstTypeRef b) { return !(a == b); } +}; + +struct ConstFuncRef { + const void* data; + ConstFuncRef() : data(nullptr) {} + ConstFuncRef(const void* P) : data(P) {} + ConstFuncRef(FuncRef d) : data(d.data) {} + ConstFuncRef(decltype(nullptr)) : data(nullptr) {} + explicit operator bool() const { return data != nullptr; } + friend bool operator==(ConstFuncRef a, ConstFuncRef b) { + return a.data == b.data; + } + friend bool operator!=(ConstFuncRef a, ConstFuncRef b) { return !(a == b); } +}; + +} // namespace Cpp + +#endif // __cplusplus + #ifdef __cplusplus #include #include @@ -50,7 +196,28 @@ #include #include -namespace CppImpl { +template <> struct std::hash { + std::size_t operator()(const Cpp::DeclRef& obj) const { + return std::hash{}(obj.data); + } +}; +template <> struct std::hash { + std::size_t operator()(const Cpp::TypeRef& obj) const { + return std::hash{}(obj.data); + } +}; +template <> struct std::hash { + std::size_t operator()(const Cpp::FuncRef& obj) const { + return std::hash{}(obj.data); + } +}; +template <> struct std::hash { + std::size_t operator()(const Cpp::ObjectRef& obj) const { + return std::hash{}(obj.data); + } +}; + +namespace Cpp { class JitCall; } namespace CppInternal { @@ -59,16 +226,15 @@ namespace DispatchRaw { // CppInterOpAPI.inc re-declares these with identical types. They're // here so JitCall::Invoke's inline body below can reference them. extern CPPINTEROP_API void (*CppInterOpTraceJitCallInvokeImpl)( - const CppImpl::JitCall* JC, void* result, void** args, std::size_t nargs, + const Cpp::JitCall* JC, void* result, void** args, std::size_t nargs, void* self); extern CPPINTEROP_API void (*CppInterOpTraceJitCallInvokeDestructorImpl)( - const CppImpl::JitCall* JC, void* object, unsigned long nary, int withFree); + const Cpp::JitCall* JC, void* object, unsigned long nary, int withFree); extern CPPINTEROP_API void (*CppInterOpTraceJitCallInvokeReturnImpl)( - const CppImpl::JitCall* JC, void* result); + const Cpp::JitCall* JC, void* result); } // namespace DispatchRaw } // namespace CppInternal -namespace CppImpl { #endif // __cplusplus /// C-compatible array of opaque pointers, returned by generated C API @@ -100,17 +266,21 @@ typedef struct TemplateArgInfo { } TemplateArgInfo; #ifdef __cplusplus - -using TCppIndex_t = size_t; -using TCppScope_t = void*; -using TCppConstScope_t = const void*; -using TCppType_t = void*; -using TCppConstType_t = const void*; -using TCppFunction_t = void*; -using TCppConstFunction_t = const void*; -using TCppFuncAddr_t = void*; -using TInterp_t = void*; -using TCppObject_t = void*; +namespace Cpp { + +// Pull file-scope C-compatible structs into the namespace. +// The handle types (DeclRef etc.) are already defined in this namespace +// above; only the genuinely-C-visible structs need pulling in. +using ::CppInterOpArray; +using ::CppInterOpStringArray; +using ::TemplateArgInfo; + +static_assert(sizeof(DeclRef) == sizeof(ConstDeclRef), + "Const/mutable handle ABI mismatch"); +static_assert(sizeof(TypeRef) == sizeof(ConstTypeRef), + "Const/mutable handle ABI mismatch"); +static_assert(sizeof(FuncRef) == sizeof(ConstFuncRef), + "Const/mutable handle ABI mismatch"); enum Operator : unsigned char { OP_None, @@ -251,8 +421,8 @@ enum class ValueKind : std::uint8_t { /// function, constructor or destructor. class JitCall { public: - friend CPPINTEROP_API JitCall MakeFunctionCallable(TInterp_t I, - TCppConstFunction_t func); + friend CPPINTEROP_API JitCall MakeFunctionCallable(InterpRef I, + ConstFuncRef func); enum Kind : char { kUnknown = 0, kGenericCall, @@ -283,13 +453,13 @@ class JitCall { DestructorCall m_DestructorCall; }; Kind m_Kind; - TCppConstFunction_t m_FD; + ConstFuncRef m_FD; JitCall() : m_GenericCall(nullptr), m_Kind(kUnknown), m_FD(nullptr) {} - JitCall(Kind K, GenericCall C, TCppConstFunction_t FD) + JitCall(Kind K, GenericCall C, ConstFuncRef FD) : m_GenericCall(C), m_Kind(K), m_FD(FD) {} - JitCall(Kind K, ConstructorCall C, TCppConstFunction_t Ctor) + JitCall(Kind K, ConstructorCall C, ConstFuncRef Ctor) : m_ConstructorCall(C), m_Kind(K), m_FD(Ctor) {} - JitCall(Kind K, DestructorCall C, TCppConstFunction_t Dtor) + JitCall(Kind K, DestructorCall C, ConstFuncRef Dtor) : m_DestructorCall(C), m_Kind(K), m_FD(Dtor) {} // Trace-hook impls need private m_FD for the function-name lookup. @@ -402,6 +572,18 @@ class JitCall { } }; +/// Opaque handle returned by MakeVTableOverlay. Owns a writable copy +/// of an instance's vtable with selected slots replaced, and the +/// original vptr so the overlay can be undone. Itanium ABI, single +/// inheritance: the offset-to-top + type_info prefix and all +/// non-overlaid slots are copied verbatim from the original vtable. +struct VTableOverlay; + +// Cleanup callback fired by VTableOverlay's destructor hook (see +// MakeVTableOverlay). Receives the original instance pointer (possibly +// dangling -- do not dereference) and the cleanup_data passed at install. +using VTableOverlayDtorHook = void (*)(void* inst, void* cleanup_data); + // FIXME: Rework GetDimensions to make this enum redundant. namespace DimensionValue { enum : long int { @@ -419,7 +601,7 @@ enum CaptureStreamKind : char { ///@} -} // namespace CppImpl +} // namespace Cpp #endif // __cplusplus #endif // CPPINTEROP_CPPINTEROPTYPES_H diff --git a/interpreter/CppInterOp/include/CppInterOp/Dispatch.h b/interpreter/CppInterOp/include/CppInterOp/Dispatch.h index eba24af9dcb8a..9a7b5a0c06864 100644 --- a/interpreter/CppInterOp/include/CppInterOp/Dispatch.h +++ b/interpreter/CppInterOp/include/CppInterOp/Dispatch.h @@ -32,6 +32,7 @@ #include #endif +#include "CppInterOp/Box.h" #include "CppInterOp/CppInterOpTypes.h" #include @@ -100,8 +101,10 @@ inline void* dlGetProcAddress(const char* name, } // Raw function pointers populated by LoadDispatchAPI. No default arguments. +// Kept in a separate namespace so the dispatch-table state doesn't pollute +// the public Cpp:: surface. namespace CppInternal::DispatchRaw { -using namespace CppImpl; +using namespace Cpp; // NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables) #define CPPINTEROP_API_FUNC(DN, CN, Ret, DeclArgs, CallArgs, RawTypes) \ extern Ret(*DN) RawTypes; @@ -109,14 +112,17 @@ using namespace CppImpl; // NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables) } // namespace CppInternal::DispatchRaw -// Inline wrappers that restore default arguments and support overloads. +// Inline wrappers go directly in namespace Cpp so that consumer source code +// (Cpp::IsAbstract, Cpp::GetNamed, ...) compiles identically against either +// CppInterOp.h (direct linking, declarations only) or Dispatch.h (inline +// bodies calling through the dispatch table). The two headers are mutually +// exclusive (#error guard at top) so only one body of Cpp::* exists per TU. // CN (CppName) may differ from DN (DispatchName) for overloaded functions, // e.g. CN=GetFunctionAddress, DN=GetFunctionAddress_fn. -namespace CppInternal::Dispatch { -using namespace CppImpl; +namespace Cpp { #define CPPINTEROP_API_FUNC(DN, CN, Ret, DeclArgs, CallArgs, RawTypes) \ - inline Ret CN DeclArgs { return DispatchRaw::DN CallArgs; } + inline Ret CN DeclArgs { return ::CppInternal::DispatchRaw::DN CallArgs; } #include "CppInterOp/CppInterOpAPI.inc" /// Initialize all CppInterOp API from the dynamically loaded library. @@ -136,11 +142,13 @@ inline bool LoadDispatchAPI(const char* customLibPath = nullptr) { // NOLINTBEGIN(cppcoreguidelines-pro-type-reinterpret-cast) #define CPPINTEROP_API_FUNC(DN, CN, Ret, DeclArgs, CallArgs, RawTypes) \ - DispatchRaw::DN = reinterpret_cast(dlGetProcAddress(#DN)); + ::CppInternal::DispatchRaw::DN = \ + reinterpret_cast(dlGetProcAddress(#DN)); #include "CppInterOp/CppInterOpAPI.inc" // NOLINTEND(cppcoreguidelines-pro-type-reinterpret-cast) - if (!DispatchRaw::GetInterpreter || !DispatchRaw::CreateInterpreter) { + if (!::CppInternal::DispatchRaw::GetInterpreter || + !::CppInternal::DispatchRaw::CreateInterpreter) { std::cerr << "[CppInterOp Dispatch] Failed to load critical functions\n"; return false; } @@ -149,12 +157,10 @@ inline bool LoadDispatchAPI(const char* customLibPath = nullptr) { inline void UnloadDispatchAPI() { #define CPPINTEROP_API_FUNC(DN, CN, Ret, DeclArgs, CallArgs, RawTypes) \ - DispatchRaw::DN = nullptr; + ::CppInternal::DispatchRaw::DN = nullptr; #include "CppInterOp/CppInterOpAPI.inc" dlGetProcAddress(nullptr, nullptr); } -} // namespace CppInternal::Dispatch +} // namespace Cpp -// NOLINTNEXTLINE(misc-unused-alias-decls) -namespace Cpp = CppInternal::Dispatch; #endif // CPPINTEROP_DISPATCH_H diff --git a/interpreter/CppInterOp/include/CppInterOp/Error.h b/interpreter/CppInterOp/include/CppInterOp/Error.h new file mode 100644 index 0000000000000..8a72fc85b39af --- /dev/null +++ b/interpreter/CppInterOp/include/CppInterOp/Error.h @@ -0,0 +1,412 @@ +//===--- Error.h - Result, ErrorRef, DiagnosticRef -----------*- C++ -*-===// +// +// Part of the compiler-research project, under the Apache License v2.0 with +// LLVM Exceptions. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +// +// Cpp::Result carries either a T or an ErrorRef. ErrorRef encodes +// either an inline Status code (no allocation) or a pointer to an +// ErrorSlice owning drained Clang diagnostics, an optional structured +// payload, and producer attribution. +// +//===----------------------------------------------------------------------===// + +#ifndef CPPINTEROP_ERROR_H +#define CPPINTEROP_ERROR_H + +#include "CppInterOp/CppInterOpTypes.h" + +#ifdef __cplusplus + +#include +#include +#include +#include +#include +#include +#include + +namespace Cpp { + +/// Outcome of a fallible API call. +enum class Status : uint8_t { + Ok = 0, + NotFound, // name lookup found nothing + InvalidArgument, // precondition violated by caller + Ambiguous, // name lookup returned multiple matches + ParseError, // Clang parser rejected the input + CompileError, // Sema/codegen rejected the input +}; + +/// Severity of a captured diagnostic. +enum class DiagnosticSeverity : uint8_t { Note, Warning, Error, Fatal }; + +/// Non-owning view over a contiguous range. Returned from accessors so +/// callers can iterate without committing to a container type. +template struct ArrayView { + const T* Data = nullptr; + size_t Size = 0; + + [[nodiscard]] const T* begin() const { return Data; } + [[nodiscard]] const T* end() const { return Data + Size; } + [[nodiscard]] size_t size() const { return Size; } + [[nodiscard]] bool empty() const { return Size == 0; } + const T& operator[](size_t I) const { return Data[I]; } +}; + +/// 8-byte opaque handle to one captured diagnostic. Valid while the +/// owning error (or interpreter) keeps its storage alive. +struct DiagnosticRef { + const void* data = nullptr; + + [[nodiscard]] bool isNull() const { return data == nullptr; } + + // Member-function surface (forwarders defined in ErrorInternal.cpp). + CPPINTEROP_API DiagnosticSeverity severity() const; + CPPINTEROP_API const char* message() const; + CPPINTEROP_API const char* file() const; + CPPINTEROP_API unsigned line() const; + CPPINTEROP_API unsigned column() const; +}; + +/// Field accessors. The TableGen binding surface; member functions +/// above forward here. Safe to call on a null DiagnosticRef: empty +/// strings, zero positions, Severity::Note. +CPPINTEROP_API DiagnosticSeverity GetDiagnosticSeverity(DiagnosticRef D); +CPPINTEROP_API const char* GetDiagnosticMessage(DiagnosticRef D); +CPPINTEROP_API const char* GetDiagnosticFile(DiagnosticRef D); +CPPINTEROP_API unsigned GetDiagnosticLine(DiagnosticRef D); +CPPINTEROP_API unsigned GetDiagnosticColumn(DiagnosticRef D); + +// +// Encoding (8 bytes, single uintptr_t): +// bits == 0 : Ok. +// bits & 1 == 1 : inline Status in bits[1..7]. No allocation. +// bits & 1 == 0, +// bits != 0 : pointer to ErrorSlice (diagnostics, payload, +// refcount). Result manages the refcount. + +struct ErrorSlice; // defined in lib/CppInterOp/ErrorInternal.h + +struct ErrorRef { + uintptr_t bits = 0; + + [[nodiscard]] bool isOk() const { return bits == 0; } + [[nodiscard]] bool isInline() const { return (bits & 1U) == 1U; } + [[nodiscard]] bool isSlice() const { return bits != 0 && (bits & 1U) == 0u; } + + /// Pack a Status code into the ErrorRef bits. Status::Ok produces + /// the canonical null encoding so isOk() stays a single comparison. + static ErrorRef makeInline(Status S) { + if (S == Status::Ok) + return ErrorRef{}; + return ErrorRef{(static_cast(S) << 1) | 1U}; + } + + /// The inline-encoded Status. Only meaningful when isInline() is + /// true; callers should prefer status() which handles both shapes. + [[nodiscard]] Status inlineStatus() const { + return static_cast(bits >> 1); + } + + /// Wrap a slice pointer. The slice's alignment must keep bit 0 clear + /// (ErrorSlice is alignas(16) for this reason). + static ErrorRef makeSlice(const ErrorSlice* S) { + return ErrorRef{reinterpret_cast(S)}; + } + [[nodiscard]] const ErrorSlice* slice() const { + return isSlice() ? reinterpret_cast(bits) : nullptr; + } + + // Member-function surface (defined in Error.cpp). + CPPINTEROP_API Status status() const; + CPPINTEROP_API ArrayView diagnostics() const; + CPPINTEROP_API const char* producer() const; + CPPINTEROP_API const char* producerSignature() const; + CPPINTEROP_API class ErrorRecord record() const; +}; + +/// Free-function aliases over the ErrorRef accessors. The TableGen +/// binding surface; the member functions above forward here. +CPPINTEROP_API Status GetStatus(ErrorRef E); +CPPINTEROP_API ArrayView GetDiagnostics(ErrorRef E); + +/// Refcount hooks. Result calls these around copy/move/dtor; they +/// are no-ops on Ok and inline encodings. +CPPINTEROP_API void RetainErrorRef(ErrorRef E); +CPPINTEROP_API void ReleaseErrorRef(ErrorRef E); + +// +// Deep-copy snapshot for callers that want a plain value-type and do +// not need the originating slice kept alive. Strings are copied; +// nothing points back into slice storage. + +struct DiagnosticInfo { + DiagnosticSeverity Severity = DiagnosticSeverity::Error; + std::string Message; + std::string File; + unsigned Line = 0; + unsigned Column = 0; +}; + +class ErrorRecord { +public: + Status Code = Status::Ok; + std::vector Diagnostics; + // Pointers into static __func__ / __PRETTY_FUNCTION__ storage. No + // copying needed; the storage outlives every record. + const char* Producer = nullptr; + const char* ProducerSignature = nullptr; +}; + +// +// Extends a slice's lifetime past the originating Result via one +// refcount bump on construction and one decrement on destruction. +// Inline errors are pure value -- no refcount work. + +class [[nodiscard]] CapturedError { + ErrorRef Ref; + +public: + CapturedError() = default; + explicit CapturedError(ErrorRef E) : Ref(E) { RetainErrorRef(Ref); } + + CapturedError(const CapturedError& O) : Ref(O.Ref) { RetainErrorRef(Ref); } + CapturedError(CapturedError&& O) noexcept : Ref(O.Ref) { O.Ref = ErrorRef{}; } + + CapturedError& operator=(const CapturedError& O) { + if (this != &O) { + ReleaseErrorRef(Ref); + Ref = O.Ref; + RetainErrorRef(Ref); + } + return *this; + } + CapturedError& operator=(CapturedError&& O) noexcept { + if (this != &O) { + ReleaseErrorRef(Ref); + Ref = O.Ref; + O.Ref = ErrorRef{}; + } + return *this; + } + + ~CapturedError() { ReleaseErrorRef(Ref); } + + [[nodiscard]] bool ok() const { return Ref.isOk(); } + explicit operator bool() const { return ok(); } + [[nodiscard]] Status status() const { return Ref.status(); } + [[nodiscard]] ErrorRef ref() const { return Ref; } + + [[nodiscard]] ArrayView diagnostics() const { + return Ref.diagnostics(); + } + [[nodiscard]] const char* producer() const { return Ref.producer(); } + [[nodiscard]] const char* producerSignature() const { + return Ref.producerSignature(); + } + [[nodiscard]] ErrorRecord record() const { return Ref.record(); } +}; + +[[noreturn]] CPPINTEROP_API void ResultAbort_ValueOnError(const ErrorRef& Err); + +#ifndef NDEBUG +[[noreturn]] CPPINTEROP_API void +ResultAbort_UncheckedOnDtor(const ErrorRef& Err); +#endif + +template class [[nodiscard]] Result { + static_assert(!std::is_same::value, + "Result is provided via specialization below"); + + static constexpr size_t kStorageSize = sizeof(T) > sizeof(ErrorRef) + ? sizeof(T) + : sizeof(ErrorRef); + static constexpr size_t kStorageAlign = alignof(T) > alignof(ErrorRef) + ? alignof(T) + : alignof(ErrorRef); + + union { + alignas(kStorageAlign) unsigned char m_ValueBytes[kStorageSize]; + ErrorRef m_ErrState; + }; + bool m_HasError = false; + +#ifndef NDEBUG + mutable bool m_Unchecked = true; + void markChecked() const { m_Unchecked = false; } +#else + void markChecked() const {} +#endif + + T* valuePtr() { return reinterpret_cast(m_ValueBytes); } + const T* valuePtr() const { return reinterpret_cast(m_ValueBytes); } + +public: + Result() { new (m_ValueBytes) T(); } + + Result(T V) { new (m_ValueBytes) T(std::move(V)); } + + Result(ErrorRef E) : m_ErrState(E), m_HasError(!E.isOk()) { + if (m_HasError) + RetainErrorRef(m_ErrState); + } + + Result(const Result& Other) : m_HasError(Other.m_HasError) { + if (m_HasError) { + m_ErrState = Other.m_ErrState; + RetainErrorRef(m_ErrState); + } else { + new (m_ValueBytes) T(*Other.valuePtr()); + } +#ifndef NDEBUG + m_Unchecked = Other.m_Unchecked; + Other.m_Unchecked = false; +#endif + } + + Result(Result&& Other) noexcept : m_HasError(Other.m_HasError) { + if (m_HasError) { + m_ErrState = Other.m_ErrState; + Other.m_ErrState = ErrorRef{}; + Other.m_HasError = false; + } else { + new (m_ValueBytes) T(std::move(*Other.valuePtr())); + } +#ifndef NDEBUG + m_Unchecked = Other.m_Unchecked; + Other.m_Unchecked = false; +#endif + } + + ~Result() { +#ifndef NDEBUG + if (m_Unchecked && m_HasError) + ResultAbort_UncheckedOnDtor(m_ErrState); +#endif + if (m_HasError) + ReleaseErrorRef(m_ErrState); + else + valuePtr()->~T(); + } + + Result& operator=(const Result&) = delete; + Result& operator=(Result&&) = delete; + + void ignore() const { markChecked(); } + + [[nodiscard]] bool ok() const { + markChecked(); + return !m_HasError; + } + explicit operator bool() const { return ok(); } + + [[nodiscard]] Status status() const { + markChecked(); + return m_HasError ? m_ErrState.status() : Status::Ok; + } + + [[nodiscard]] ErrorRef error() const { + markChecked(); + return m_HasError ? m_ErrState : ErrorRef{}; + } + + /// Share ownership of the carried ErrorRef with a CapturedError so + /// the error outlives this Result. One refcount bump on slice + /// errors; no-op for inline ones. + [[nodiscard]] CapturedError share() const { + markChecked(); + return m_HasError ? CapturedError(m_ErrState) : CapturedError(); + } + + T value() const { + markChecked(); + if (m_HasError) + ResultAbort_ValueOnError(m_ErrState); + return *valuePtr(); + } + + T value_or(T fallback) const { + markChecked(); + return m_HasError ? std::move(fallback) : *valuePtr(); + } +}; + +// Void specialization. + +template <> class [[nodiscard]] Result { + ErrorRef m_ErrState; +#ifndef NDEBUG + mutable bool m_Unchecked = true; + void markChecked() const { m_Unchecked = false; } +#else + void markChecked() const {} +#endif + +public: + Result() = default; + Result(ErrorRef E) : m_ErrState(E) { + if (!E.isOk()) + RetainErrorRef(m_ErrState); + } + + Result(const Result& O) : m_ErrState(O.m_ErrState) { + if (!m_ErrState.isOk()) + RetainErrorRef(m_ErrState); +#ifndef NDEBUG + m_Unchecked = O.m_Unchecked; + O.m_Unchecked = false; +#endif + } + + Result(Result&& O) noexcept : m_ErrState(O.m_ErrState) { +#ifndef NDEBUG + m_Unchecked = O.m_Unchecked; + O.m_Unchecked = false; +#endif + O.m_ErrState = ErrorRef{}; + } + + ~Result() { +#ifndef NDEBUG + if (m_Unchecked && !m_ErrState.isOk()) + ResultAbort_UncheckedOnDtor(m_ErrState); +#endif + if (!m_ErrState.isOk()) + ReleaseErrorRef(m_ErrState); + } + + Result& operator=(const Result&) = delete; + Result& operator=(Result&&) = delete; + + void ignore() const { markChecked(); } + + [[nodiscard]] bool ok() const { + markChecked(); + return m_ErrState.isOk(); + } + explicit operator bool() const { return ok(); } + + [[nodiscard]] Status status() const { + markChecked(); + return m_ErrState.status(); + } + + [[nodiscard]] ErrorRef error() const { + markChecked(); + return m_ErrState; + } + + [[nodiscard]] CapturedError share() const { + markChecked(); + return CapturedError(m_ErrState); + } +}; + +} // namespace Cpp + +#endif // __cplusplus + +#endif // CPPINTEROP_ERROR_H diff --git a/interpreter/CppInterOp/lib/CppInterOp/CMakeLists.txt b/interpreter/CppInterOp/lib/CppInterOp/CMakeLists.txt index 398c31be9942b..e184c930b9bbb 100644 --- a/interpreter/CppInterOp/lib/CppInterOp/CMakeLists.txt +++ b/interpreter/CppInterOp/lib/CppInterOp/CMakeLists.txt @@ -56,6 +56,15 @@ endif() # Get rid of libLLVM-X.so which is appended to the list of static libraries. if (LLVM_LINK_LLVM_DYLIB) + # These make up for symbols the stripped libLLVM.so used to provide. Seed them + # before the walk below, not after: the walk only drops the shared LLVM + # dependency from the targets it visits, and clangStaticAnalyzerCore is not + # reachable from the other link_libs -- appending it later leaves libLLVM.so + # in DT_NEEDED. + list(APPEND link_libs + clangCodeGen + clangStaticAnalyzerCore + ) set(new_libs ${link_libs}) set(libs ${new_libs}) while(NOT "${new_libs}" STREQUAL "") @@ -95,12 +104,8 @@ if (LLVM_LINK_LLVM_DYLIB) Coverage FrontendHLSL LTO - ) - # We will need to append the missing dependencies to pull in the right - # LLVM library dependencies. - list(APPEND link_libs - clangCodeGen - clangStaticAnalyzerCore + Option + TargetParser ) endif(LLVM_LINK_LLVM_DYLIB) @@ -213,6 +218,7 @@ add_llvm_library(clangCppInterOp CppInterOpDispatch.cpp CXCppInterOp.cpp CXCppInterOpGenerated.cpp + ErrorInternal.cpp Tracing.cpp ${DLM} LINK_LIBS diff --git a/interpreter/CppInterOp/lib/CppInterOp/CXCppInterOp.cpp b/interpreter/CppInterOp/lib/CppInterOp/CXCppInterOp.cpp index c73e66488daa8..09c32cfa74964 100644 --- a/interpreter/CppInterOp/lib/CppInterOp/CXCppInterOp.cpp +++ b/interpreter/CppInterOp/lib/CppInterOp/CXCppInterOp.cpp @@ -1,20 +1,71 @@ // Hand-written C API wrappers for functions the TableGen emitter -// cannot generate mechanically. Generated wrappers live in +// cannot generate mechanically, plus C-ABI-shaped overloads of Cpp:: +// APIs whose primary form (in CppInterOp.cpp) cannot cross the C +// boundary. Public declarations of these symbols live in +// include/CppInterOp/CXCppInterOp.h; generated wrappers live in // CXCppInterOpGenerated.cpp. +#include "CppInterOp/CXCppInterOp.h" +#include "Unwrap.h" #include "CppInterOp/CppInterOp.h" +#include "Compatibility.h" + #include #include #include +#if defined(__has_feature) +#if __has_feature(memory_sanitizer) +#include +#define CPPINTEROP_MSAN_UNPOISON_VALUE(v) __msan_unpoison(&(v), sizeof(v)) +#else +#define CPPINTEROP_MSAN_UNPOISON_VALUE(v) ((void)0) +#endif +#else +#define CPPINTEROP_MSAN_UNPOISON_VALUE(v) ((void)0) +#endif + +namespace Cpp { + +// Legacy C-ABI overload of Cpp::Evaluate. The Box-returning overload in +// CppInterOp.cpp cannot cross the C boundary; bindings that go through +// the generated cppinterop_Evaluate_intptr wrapper (e.g. cppyy) land +// here instead. +intptr_t Evaluate(const char* code, bool* HadError) { + auto* I = unwrap(GetInterpreter()); + compat::Value V; + + if (HadError) + *HadError = false; + + auto res = I->evaluate(code, V); + CPPINTEROP_MSAN_UNPOISON_VALUE(V); + if (res != 0 || !V.hasValue()) { + if (HadError) + *HadError = true; + // FIXME: Make this return llvm::Expected. + return ~0UL; + } + + return compat::convertTo(V); +} + +} // namespace Cpp + extern "C" { +// C-ABI bridge for the intptr_t Cpp::Evaluate overload above. The Box- +// returning overload generated from CppInterOp.td is C++-only. +CPPINTEROP_API intptr_t cppinterop_Evaluate(const char* code, bool* HadError) { + return Cpp::Evaluate(code, HadError); +} + // GetClassTemplatedMethods returns bool AND fills a vector out-param. // The C wrapper drops the bool (caller checks arr.size > 0 instead). CPPINTEROP_API Cpp::CppInterOpArray -cppinterop_GetClassTemplatedMethods(const char* name, void* parent) { - std::vector out; +cppinterop_GetClassTemplatedMethods(const char* name, CppConstDeclRef parent) { + std::vector out; Cpp::GetClassTemplatedMethods(std::string(name), parent, out); Cpp::CppInterOpArray arr = {nullptr, out.size()}; if (arr.size) { diff --git a/interpreter/CppInterOp/lib/CppInterOp/CXCppInterOpGenerated.cpp b/interpreter/CppInterOp/lib/CppInterOp/CXCppInterOpGenerated.cpp index 10060fc82891e..65891b264233b 100644 --- a/interpreter/CppInterOp/lib/CppInterOp/CXCppInterOpGenerated.cpp +++ b/interpreter/CppInterOp/lib/CppInterOp/CXCppInterOpGenerated.cpp @@ -6,9 +6,27 @@ #include #include -using Cpp::CppInterOpArray; -using Cpp::CppInterOpStringArray; -using Cpp::TemplateArgInfo; +// The generated Impl.inc uses the prefixed C-side names (CppDeclRef etc.) +// in extern "C" signatures. Alias them to the namespaced Cpp::* types so +// the wrapper compiles; layout-identical, no conversion at call sites. +using CppDeclRef = Cpp::DeclRef; +using CppTypeRef = Cpp::TypeRef; +using CppFuncRef = Cpp::FuncRef; +using CppObjectRef = Cpp::ObjectRef; +using CppInterpRef = Cpp::InterpRef; +using CppConstDeclRef = Cpp::ConstDeclRef; +using CppConstTypeRef = Cpp::ConstTypeRef; +using CppConstFuncRef = Cpp::ConstFuncRef; +// Unprefixed bare names — the emitter constructs std::vector +// etc. as out-param scratch buffers inside wrapper bodies. +using Cpp::ConstDeclRef; +using Cpp::ConstFuncRef; +using Cpp::ConstTypeRef; +using Cpp::DeclRef; +using Cpp::FuncRef; +using Cpp::InterpRef; +using Cpp::ObjectRef; +using Cpp::TypeRef; extern "C" { #include "CppInterOp/CXCppInterOpImpl.inc" diff --git a/interpreter/CppInterOp/lib/CppInterOp/Compatibility.h b/interpreter/CppInterOp/lib/CppInterOp/Compatibility.h index 294c6b1b960f1..a62623f42da02 100644 --- a/interpreter/CppInterOp/lib/CppInterOp/Compatibility.h +++ b/interpreter/CppInterOp/lib/CppInterOp/Compatibility.h @@ -5,6 +5,7 @@ #ifndef CPPINTEROP_COMPATIBILITY_H #define CPPINTEROP_COMPATIBILITY_H +#include "clang/AST/Decl.h" #include "clang/AST/DeclTemplate.h" #include "clang/AST/GlobalDecl.h" #include "clang/Basic/DiagnosticIDs.h" @@ -32,6 +33,9 @@ #include "llvm/ADT/SmallVector.h" #include "llvm/Support/FileSystem.h" +#include "CppInterOp/Box.h" + +#include #include #include #include @@ -49,12 +53,6 @@ #define Suppress_Elab FullyQualifiedName #endif -#if CLANG_VERSION_MAJOR < 22 -#define Get_Tag_Type getTagDeclType -#else -#define Get_Tag_Type getCanonicalTagType -#endif - #ifdef _MSC_VER #define dup _dup #define dup2 _dup2 @@ -698,6 +696,13 @@ class SynthesizingCodeRAII { namespace compat { +// QualType for a TypeDecl. Pass a TypeDecl base pointer: Clang 22 deleted the +// TagDecl/TypedefDecl overloads, but the surviving TypeDecl one dispatches to +// getCanonicalTagType for tags, covering all decl kinds on Clang 21 and 22. +inline clang::QualType GetTypeFromDecl(const clang::TypeDecl* TD) { + return TD->getASTContext().getTypeDeclType(TD); +} + #ifdef CPPINTEROP_USE_CLING using Value = cling::Value; #else @@ -715,6 +720,42 @@ template inline T convertTo(clang::Value V) { } #endif // CPPINTEROP_USE_CLING +// Refcount-shared payload wrapping a `compat::Value` for Cpp::Box's +// K_PtrOrObj slot. Boxing-via-copy (not move): clang::Value's move ctor +// releases its own storage on construction -- fixed upstream by +// llvm/llvm-project#200888. The copy ctor correctly retains. +// FIXME(llvm 23): static_assert below fails the build once the minimum +// LLVM crosses 23, prompting the move-semantics cleanup. +static_assert(LLVM_VERSION_MAJOR < 23, + "clang::Value::Value(Value&&) was fixed upstream in " + "llvm/llvm-project#200888; switch ValueRefCount to move " + "semantics and drop this workaround."); + +namespace detail { +struct ValueRefCount { + std::atomic rc; + Value v; + explicit ValueRefCount(const Value& V) noexcept : rc(1), v(V) {} + + static void retain(void* p) noexcept { + static_cast(p)->rc.fetch_add(1, std::memory_order_relaxed); + } + static void release(void* p) noexcept { + auto* rc = static_cast(p); + if (rc->rc.fetch_sub(1, std::memory_order_acq_rel) == 1) + delete rc; + } + static constexpr Cpp::Box::ObjectOps Ops{&retain, &release}; +}; +} // namespace detail + +/// Wrap a compat::Value into a refcount-shared K_PtrOrObj Cpp::Box. +/// `qt` is the opaque QualType (clang::QualType::getAsOpaquePtr()). +inline Cpp::Box MakeValueBox(const Value& V, void* qt) noexcept { + return Cpp::Box::AdoptObject(new detail::ValueRefCount(V), + &detail::ValueRefCount::Ops, qt); +} + inline void InstantiateClassTemplateSpecialization( Interpreter& interp, clang::ClassTemplateSpecializationDecl* CTSD) { #ifdef CPPINTEROP_USE_CLING diff --git a/interpreter/CppInterOp/lib/CppInterOp/CppInterOp.cpp b/interpreter/CppInterOp/lib/CppInterOp/CppInterOp.cpp old mode 100755 new mode 100644 index b00684a190c34..1a20f22527164 --- a/interpreter/CppInterOp/lib/CppInterOp/CppInterOp.cpp +++ b/interpreter/CppInterOp/lib/CppInterOp/CppInterOp.cpp @@ -8,8 +8,12 @@ //------------------------------------------------------------------------------ #include "CppInterOp/CppInterOp.h" +#include "Unwrap.h" +#include "CppInterOp/Error.h" #include "Compatibility.h" +#include "ErrorInternal.h" +#include "InterpreterInfo.h" #include "Sins.h" // for access to private members #include "Tracing.h" @@ -36,6 +40,7 @@ #include "clang/AST/DeclAccessPair.h" #include "clang/AST/DeclBase.h" #include "clang/AST/DeclCXX.h" +#include "clang/AST/DeclTemplate.h" #include "clang/AST/DeclarationName.h" #include "clang/AST/Expr.h" #include "clang/AST/ExprCXX.h" @@ -47,6 +52,8 @@ #include "clang/AST/RecordLayout.h" #include "clang/AST/Stmt.h" #include "clang/AST/Type.h" +#include "clang/AST/VTableBuilder.h" +#include "clang/Basic/CharInfo.h" #include "clang/Basic/Diagnostic.h" #include "clang/Basic/DiagnosticSema.h" #include "clang/Basic/LangStandard.h" @@ -65,6 +72,7 @@ #include "clang/Sema/Sema.h" #include "clang/Sema/TemplateDeduction.h" +#include "llvm/ADT/STLExtras.h" #include "llvm/ADT/SmallString.h" #include "llvm/ADT/SmallVector.h" #include "llvm/ADT/StringRef.h" @@ -90,11 +98,13 @@ #include "llvm/TargetParser/Triple.h" #include +#include #include #include #include #include #include +#include #include #include #include @@ -154,55 +164,11 @@ extern "C" void __clang_Interpreter_SetValueNoAlloc(void* This, void* OutVal, #define CPPINTEROP_ASAN_BUILD 1 #endif -namespace CppImpl { +namespace Cpp { using namespace clang; using namespace llvm; -struct InterpreterInfo { - compat::Interpreter* Interpreter = nullptr; - bool isOwned = true; - // Store the list of builtin types. - llvm::StringMap BuiltinMap; - // Per-interpreter wrapper caches. Keyed on AST nodes that belong to this - // interpreter, so the caches must be destroyed together with it. - std::map WrapperStore; - std::map DtorWrapperStore; - - InterpreterInfo(compat::Interpreter* I, bool Owned) - : Interpreter(I), isOwned(Owned) {} - - // Enable move constructors. - InterpreterInfo(InterpreterInfo&& other) noexcept - : Interpreter(other.Interpreter), isOwned(other.isOwned) { - other.Interpreter = nullptr; - other.isOwned = false; - } - InterpreterInfo& operator=(InterpreterInfo&& other) noexcept { - if (this != &other) { - // Delete current resource if owned - if (isOwned) - delete Interpreter; - - Interpreter = other.Interpreter; - isOwned = other.isOwned; - - other.Interpreter = nullptr; - other.isOwned = false; - } - return *this; - } - - ~InterpreterInfo() { - if (isOwned) - delete Interpreter; - } - - // Disable copy semantics (to avoid accidental double deletes) - InterpreterInfo(const InterpreterInfo&) = delete; - InterpreterInfo& operator=(const InterpreterInfo&) = delete; -}; - static void DefaultProcessCrashHandler(void*); /// Set by UseExternalInterpreter to suppress llvm_shutdown at process exit @@ -307,6 +273,7 @@ static void DefaultProcessCrashHandler(void*) { static void RegisterInterpreter(compat::Interpreter* I, bool Owned) { std::deque& Interps = GetInterpreters(Owned); Interps.emplace_back(I, Owned); + InstallDiagConsumer(&Interps.back()); } static InterpreterInfo& getInterpInfo(compat::Interpreter* I = nullptr) { @@ -321,13 +288,17 @@ static InterpreterInfo& getInterpInfo(compat::Interpreter* I = nullptr) { return Interps.back(); } -static compat::Interpreter& getInterp(TInterp_t I = nullptr) { +static compat::Interpreter& getInterp(InterpRef I = nullptr) { if (I) - return *static_cast(I); + return *unwrap(I); return *getInterpInfo().Interpreter; } -TInterp_t GetInterpreter() { +CPPINTEROP_API InterpreterInfo* GetInterpInfo(InterpRef I) { + return &getInterpInfo(I ? unwrap(I) : nullptr); +} + +InterpRef GetInterpreter() { INTEROP_TRACE(); std::deque& Interps = GetInterpreters(); if (Interps.empty()) @@ -335,23 +306,25 @@ TInterp_t GetInterpreter() { return INTEROP_RETURN(Interps.back().Interpreter); } -void UseExternalInterpreter(TInterp_t I) { +void UseExternalInterpreter(InterpRef I) { INTEROP_TRACE(I); assert(GetInterpreters(false).empty() && "sInterpreter already in use!"); SkipShutDown = true; - RegisterInterpreter(static_cast(I), /*Owned=*/false); + RegisterInterpreter(unwrap(I), /*Owned=*/false); return INTEROP_VOID_RETURN(); } -bool ActivateInterpreter(TInterp_t I) { +bool ActivateInterpreter(InterpRef I) { INTEROP_TRACE(I); if (!I) return INTEROP_RETURN(false); std::deque& Interps = GetInterpreters(); + auto* Interp = unwrap(I); auto found = - std::find_if(Interps.begin(), Interps.end(), - [&I](const auto& Info) { return Info.Interpreter == I; }); + std::find_if(Interps.begin(), Interps.end(), [Interp](const auto& Info) { + return Info.Interpreter == Interp; + }); if (found == Interps.end()) return INTEROP_RETURN(false); @@ -361,7 +334,7 @@ bool ActivateInterpreter(TInterp_t I) { return INTEROP_RETURN(true); // success } -bool DeleteInterpreter(TInterp_t I /*=nullptr*/) { +bool DeleteInterpreter(InterpRef I /*=nullptr*/) { INTEROP_TRACE(I); std::deque& Interps = GetInterpreters(); if (Interps.empty()) @@ -372,9 +345,11 @@ bool DeleteInterpreter(TInterp_t I /*=nullptr*/) { return INTEROP_RETURN(true); } + auto* Interp = unwrap(I); auto found = - std::find_if(Interps.begin(), Interps.end(), - [&I](const auto& Info) { return Info.Interpreter == I; }); + std::find_if(Interps.begin(), Interps.end(), [Interp](const auto& Info) { + return Info.Interpreter == Interp; + }); if (found == Interps.end()) return INTEROP_RETURN(false); // failure @@ -442,9 +417,9 @@ bool JitCall::AreArgumentsValid(void* result, ArgList args, void* self, assert(self && "Must pass the pointer to object"); Valid &= (bool)self; } - const auto* FD = cast((const Decl*)m_FD); + const auto* FD = cast(unwrap(m_FD)); if (!FD->getReturnType()->isVoidType() && !result) { - assert(0 && "We are discarding the return type of the function!"); + assert(0 && "We are discarding the return TyRef of the function!"); Valid = false; } if (Cpp::IsConstructor(m_FD) && nary == 0UL) { @@ -452,7 +427,7 @@ bool JitCall::AreArgumentsValid(void* result, ArgList args, void* self, Valid = false; } if (Cpp::IsConstructor(m_FD)) { - const auto* CD = cast((const Decl*)m_FD); + const auto* CD = cast(unwrap(m_FD)); if (CD->getMinRequiredArguments() != 0 && nary > 1) { assert(0 && "Cannot pass initialization parameters to array new construction"); @@ -474,7 +449,7 @@ void CppInterOpTraceJitCallInvokeImpl(const JitCall* JC, void* result, return; std::string Name; llvm::raw_string_ostream OS(Name); - const auto* FD = static_cast(JC->m_FD); + const auto* FD = unwrap(JC->m_FD); FD->getNameForDiagnostic(OS, FD->getASTContext().getPrintingPolicy(), /*Qualified=*/true); LLVM_DEBUG(dbgs() << "Run '" << Name << "', compiled at: " @@ -495,7 +470,7 @@ void CppInterOpTraceJitCallInvokeDestructorImpl(const JitCall* JC, void* object, return; std::string Name; llvm::raw_string_ostream OS(Name); - const auto* FD = static_cast(JC->m_FD); + const auto* FD = unwrap(JC->m_FD); FD->getNameForDiagnostic(OS, FD->getASTContext().getPrintingPolicy(), /*Qualified=*/true); LLVM_DEBUG(dbgs() << "Finish '" << Name @@ -515,7 +490,7 @@ void CppInterOpTraceJitCallInvokeReturnImpl(const JitCall* JC, void* result) { auto* TI = CppInterOp::Tracing::TheTraceInfo; if (!TI || !result) return; - const auto* FD = static_cast(JC->m_FD); + const auto* FD = unwrap(JC->m_FD); bool RegisterPtr = isa(FD); if (!RegisterPtr) { QualType RT = FD->getReturnType(); @@ -599,51 +574,51 @@ static void InstantiateFunctionDefinition(Decl* D) { } } -bool IsAggregate(TCppScope_t scope) { - INTEROP_TRACE(scope); - Decl* D = static_cast(scope); +bool IsAggregate(ConstDeclRef DRef) { + INTEROP_TRACE(DRef); + const auto* D = unwrap(DRef); // Aggregates are only arrays or tag decls. - if (ValueDecl* ValD = dyn_cast(D)) + if (const auto* ValD = dyn_cast(D)) if (ValD->getType()->isArrayType()) return INTEROP_RETURN(true); // struct, class, union - if (CXXRecordDecl* CXXRD = dyn_cast(D)) + if (const auto* CXXRD = dyn_cast(D)) return INTEROP_RETURN(CXXRD->isAggregate()); return INTEROP_RETURN(false); } -bool IsNamespace(TCppScope_t scope) { - INTEROP_TRACE(scope); - Decl* D = static_cast(scope); +bool IsNamespace(ConstDeclRef DRef) { + INTEROP_TRACE(DRef); + const auto* D = unwrap(DRef); return INTEROP_RETURN(isa(D)); } -bool IsClass(TCppScope_t scope) { - INTEROP_TRACE(scope); - Decl* D = static_cast(scope); +bool IsClass(ConstDeclRef DRef) { + INTEROP_TRACE(DRef); + const auto* D = unwrap(DRef); return INTEROP_RETURN(isa(D)); } -bool IsFunction(TCppScope_t scope) { - INTEROP_TRACE(scope); - Decl* D = static_cast(scope); +bool IsFunction(ConstDeclRef DRef) { + INTEROP_TRACE(DRef); + const auto* D = unwrap(DRef); return INTEROP_RETURN(isa(D)); } -bool IsFunctionPointerType(TCppType_t type) { - INTEROP_TRACE(type); - QualType QT = QualType::getFromOpaquePtr(type); +bool IsFunctionPointerType(ConstTypeRef TyRef) { + INTEROP_TRACE(TyRef); + QualType QT = QualType::getFromOpaquePtr(TyRef.data); return INTEROP_RETURN(QT->isFunctionPointerType()); } -bool IsClassPolymorphic(TCppScope_t klass) { - INTEROP_TRACE(klass); - Decl* D = static_cast(klass); - if (auto* CXXRD = llvm::dyn_cast(D)) - if (auto* CXXRDD = CXXRD->getDefinition()) +bool IsClassPolymorphic(ConstDeclRef DRef) { + INTEROP_TRACE(DRef); + const auto* D = unwrap(DRef); + if (const auto* CXXRD = llvm::dyn_cast(D)) + if (const auto* CXXRDD = CXXRD->getDefinition()) return INTEROP_RETURN(CXXRDD->isPolymorphic()); return INTEROP_RETURN(false); } @@ -654,37 +629,37 @@ static SourceLocation GetValidSLoc(Sema& semaRef) { } // See TClingClassInfo::IsLoaded -bool IsComplete(TCppScope_t scope) { - INTEROP_TRACE(scope); - if (!scope) +bool IsComplete(ConstDeclRef DRef) { + INTEROP_TRACE(DRef); + if (!DRef) return INTEROP_RETURN(false); - Decl* D = static_cast(scope); + const auto* D = unwrap(DRef); if (isa(D)) { - QualType QT = QualType::getFromOpaquePtr(GetTypeFromScope(scope)); + QualType QT = QualType::getFromOpaquePtr(GetTypeFromScope(DRef).data); clang::Sema& S = getSema(); SourceLocation fakeLoc = GetValidSLoc(S); compat::SynthesizingCodeRAII RAII(&getInterp()); return INTEROP_RETURN(S.isCompleteType(fakeLoc, QT)); } - if (auto* CXXRD = dyn_cast(D)) + if (const auto* CXXRD = dyn_cast(D)) return INTEROP_RETURN(CXXRD->hasDefinition()); - else if (auto* TD = dyn_cast(D)) + else if (const auto* TD = dyn_cast(D)) return INTEROP_RETURN(TD->getDefinition()); // Everything else is considered complete. return INTEROP_RETURN(true); } -size_t SizeOf(TCppScope_t scope) { - INTEROP_TRACE(scope); - assert(scope); - if (!IsComplete(scope)) +size_t SizeOf(ConstDeclRef DRef) { + INTEROP_TRACE(DRef); + assert(DRef); + if (!IsComplete(DRef)) return INTEROP_RETURN(0); - if (auto* RD = dyn_cast(static_cast(scope))) { + if (const auto* RD = dyn_cast(unwrap(DRef))) { ASTContext& Context = RD->getASTContext(); const ASTRecordLayout& Layout = Context.getASTRecordLayout(RD); return INTEROP_RETURN(Layout.getSize().getQuantity()); @@ -693,9 +668,9 @@ size_t SizeOf(TCppScope_t scope) { return INTEROP_RETURN(0); } -bool IsBuiltin(TCppConstType_t type) { - INTEROP_TRACE(type); - QualType Ty = QualType::getFromOpaquePtr(type); +bool IsBuiltin(ConstTypeRef TyRef) { + INTEROP_TRACE(TyRef); + QualType Ty = QualType::getFromOpaquePtr(TyRef.data); if (Ty->isBuiltinType() || Ty->isAnyComplexType()) return INTEROP_RETURN(true); // Check for std::complex specializations. @@ -710,49 +685,49 @@ bool IsBuiltin(TCppConstType_t type) { return INTEROP_RETURN(false); } -bool IsTemplate(TCppScope_t handle) { - INTEROP_TRACE(handle); - auto* D = (clang::Decl*)handle; +bool IsTemplate(ConstDeclRef DRef) { + INTEROP_TRACE(DRef); + const auto* D = unwrap(DRef); return INTEROP_RETURN(llvm::isa_and_nonnull(D)); } -bool IsTemplateSpecialization(TCppScope_t handle) { - INTEROP_TRACE(handle); - auto* D = (clang::Decl*)handle; +bool IsTemplateSpecialization(ConstDeclRef DRef) { + INTEROP_TRACE(DRef); + const auto* D = unwrap(DRef); return INTEROP_RETURN( llvm::isa_and_nonnull(D)); } -bool IsTypedefed(TCppScope_t handle) { - INTEROP_TRACE(handle); - auto* D = (clang::Decl*)handle; +bool IsTypedefed(ConstDeclRef DRef) { + INTEROP_TRACE(DRef); + const auto* D = unwrap(DRef); return INTEROP_RETURN(llvm::isa_and_nonnull(D)); } -bool IsAbstract(TCppType_t klass) { - INTEROP_TRACE(klass); - auto* D = (clang::Decl*)klass; - if (auto* CXXRD = llvm::dyn_cast_or_null(D)) +bool IsAbstract(ConstDeclRef DRef) { + INTEROP_TRACE(DRef); + const auto* D = unwrap(DRef); + if (const auto* CXXRD = llvm::dyn_cast_or_null(D)) return INTEROP_RETURN(CXXRD->isAbstract()); return INTEROP_RETURN(false); } -bool IsEnumScope(TCppScope_t handle) { - INTEROP_TRACE(handle); - auto* D = (clang::Decl*)handle; +bool IsEnumScope(ConstDeclRef DRef) { + INTEROP_TRACE(DRef); + const auto* D = unwrap(DRef); return INTEROP_RETURN(llvm::isa_and_nonnull(D)); } -bool IsEnumConstant(TCppScope_t handle) { - INTEROP_TRACE(handle); - auto* D = (clang::Decl*)handle; +bool IsEnumConstant(ConstDeclRef DRef) { + INTEROP_TRACE(DRef); + const auto* D = unwrap(DRef); return INTEROP_RETURN(llvm::isa_and_nonnull(D)); } -bool IsEnumType(TCppType_t type) { - INTEROP_TRACE(type); - QualType QT = QualType::getFromOpaquePtr(type); +bool IsEnumType(ConstTypeRef TyRef) { + INTEROP_TRACE(TyRef); + QualType QT = QualType::getFromOpaquePtr(TyRef.data); return INTEROP_RETURN(QT->isEnumeralType()); } @@ -778,7 +753,7 @@ static bool isSmartPointer(const RecordType* RT) { if (foundStarOperator && foundArrowOperator) return true; - const CXXRecordDecl* CXXRecord = dyn_cast(Record); + const auto* CXXRecord = dyn_cast(Record); if (!CXXRecord) return false; @@ -798,12 +773,12 @@ static bool isSmartPointer(const RecordType* RT) { return !CXXRecord->forallBases(FindOverloadedOperators); } -bool IsSmartPtrType(TCppType_t type) { - INTEROP_TRACE(type); - QualType QT = QualType::getFromOpaquePtr(type); +bool IsSmartPtrType(ConstTypeRef TyRef) { + INTEROP_TRACE(TyRef); + QualType QT = QualType::getFromOpaquePtr(TyRef.data); if (const RecordType* RT = QT->getAs()) { // Add quick checks for the std smart prts to cover most of the cases. - std::string typeString = GetTypeAsString(type); + std::string typeString = GetTypeAsString(TyRef); llvm::StringRef tsRef(typeString); if (tsRef.starts_with("std::unique_ptr") || tsRef.starts_with("std::shared_ptr") || @@ -814,104 +789,104 @@ bool IsSmartPtrType(TCppType_t type) { return INTEROP_RETURN(false); } -TCppType_t GetIntegerTypeFromEnumScope(TCppScope_t handle) { - INTEROP_TRACE(handle); - auto* D = (clang::Decl*)handle; - if (auto* ED = llvm::dyn_cast_or_null(D)) { +TypeRef GetIntegerTypeFromEnumScope(ConstDeclRef DRef) { + INTEROP_TRACE(DRef); + const auto* D = unwrap(DRef); + if (const auto* ED = llvm::dyn_cast_or_null(D)) { return INTEROP_RETURN(ED->getIntegerType().getAsOpaquePtr()); } return INTEROP_RETURN(nullptr); } -TCppType_t GetIntegerTypeFromEnumType(TCppType_t enum_type) { +TypeRef GetIntegerTypeFromEnumType(ConstTypeRef enum_type) { INTEROP_TRACE(enum_type); if (!enum_type) return INTEROP_RETURN(nullptr); - QualType QT = QualType::getFromOpaquePtr(enum_type); - if (auto* ET = QT->getAs()) + QualType QT = QualType::getFromOpaquePtr(enum_type.data); + if (const auto* ET = QT->getAs()) return INTEROP_RETURN(ET->getDecl()->getIntegerType().getAsOpaquePtr()); return INTEROP_RETURN(nullptr); } -std::vector GetEnumConstants(TCppScope_t handle) { - INTEROP_TRACE(handle); - auto* D = (clang::Decl*)handle; +std::vector GetEnumConstants(ConstDeclRef DRef) { + INTEROP_TRACE(DRef); + const auto* D = unwrap(DRef); - if (auto* ED = llvm::dyn_cast_or_null(D)) { - std::vector enum_constants; + if (const auto* ED = llvm::dyn_cast_or_null(D)) { + std::vector enum_constants; for (auto* ECD : ED->enumerators()) { - enum_constants.push_back((TCppScope_t)ECD); + enum_constants.push_back(ECD); } return INTEROP_RETURN(enum_constants); } - return INTEROP_RETURN(std::vector{}); + return INTEROP_RETURN(std::vector{}); } -TCppType_t GetEnumConstantType(TCppScope_t handle) { - INTEROP_TRACE(handle); - if (!handle) +TypeRef GetEnumConstantType(ConstDeclRef DRef) { + INTEROP_TRACE(DRef); + if (!DRef) return INTEROP_RETURN(nullptr); - auto* D = (clang::Decl*)handle; - if (auto* ECD = llvm::dyn_cast(D)) + const auto* D = unwrap(DRef); + if (const auto* ECD = llvm::dyn_cast(D)) return INTEROP_RETURN(ECD->getType().getAsOpaquePtr()); return INTEROP_RETURN(nullptr); } -TCppIndex_t GetEnumConstantValue(TCppScope_t handle) { - INTEROP_TRACE(handle); - auto* D = (clang::Decl*)handle; - if (auto* ECD = llvm::dyn_cast_or_null(D)) { +size_t GetEnumConstantValue(ConstDeclRef DRef) { + INTEROP_TRACE(DRef); + const auto* D = unwrap(DRef); + if (const auto* ECD = llvm::dyn_cast_or_null(D)) { const llvm::APSInt& Val = ECD->getInitVal(); return INTEROP_RETURN(Val.getExtValue()); } return INTEROP_RETURN(0); } -size_t GetSizeOfType(TCppType_t type) { - INTEROP_TRACE(type); - QualType QT = QualType::getFromOpaquePtr(type); +size_t GetSizeOfType(ConstTypeRef TyRef) { + INTEROP_TRACE(TyRef); + QualType QT = QualType::getFromOpaquePtr(TyRef.data); if (const TagType* TT = QT->getAs()) return INTEROP_RETURN(SizeOf(TT->getDecl())); - // FIXME: Can we get the size of a non-tag type? + // FIXME: Can we get the size of a non-tag TyRef? auto TI = getSema().getASTContext().getTypeInfo(QT); size_t TypeSize = TI.Width; return INTEROP_RETURN(TypeSize / 8); } -bool IsVariable(TCppScope_t scope) { - INTEROP_TRACE(scope); - auto* D = (clang::Decl*)scope; +bool IsVariable(ConstDeclRef DRef) { + INTEROP_TRACE(DRef); + const auto* D = unwrap(DRef); return INTEROP_RETURN(llvm::isa_and_nonnull(D)); } -std::string GetName(TCppType_t klass) { - INTEROP_TRACE(klass); - auto* D = (clang::NamedDecl*)klass; +std::string GetName(ConstDeclRef DRef) { + INTEROP_TRACE(DRef); + const auto* D = unwrap(DRef); if (llvm::isa_and_nonnull(D)) { return INTEROP_RETURN(""); } - if (auto* ND = llvm::dyn_cast_or_null(D)) { + if (const auto* ND = llvm::dyn_cast_or_null(D)) { return INTEROP_RETURN(ND->getNameAsString()); } return INTEROP_RETURN(""); } -static std::string GetCompleteNameImpl(TCppType_t klass, bool qualified) { +static std::string GetCompleteNameImpl(ConstDeclRef DRef, bool qualified) { auto& C = getSema().getASTContext(); - auto* D = (Decl*)klass; + const auto* D = unwrap(DRef); - if (auto* ND = llvm::dyn_cast_or_null(D)) { + if (const auto* ND = llvm::dyn_cast_or_null(D)) { PrintingPolicy Policy = C.getPrintingPolicy(); Policy.SuppressUnwrittenScope = true; if (qualified) { @@ -925,13 +900,13 @@ static std::string GetCompleteNameImpl(TCppType_t klass, bool qualified) { Policy.AlwaysIncludeTypeForTemplateArgument = true; } - if (auto* TD = llvm::dyn_cast(ND)) { + if (const auto* TD = llvm::dyn_cast(ND)) { std::string type_name; - QualType QT = C.Get_Tag_Type(TD); + QualType QT = compat::GetTypeFromDecl(TD); QT.getAsStringInternal(type_name, Policy); return type_name; } - if (auto* FD = llvm::dyn_cast(ND)) { + if (const auto* FD = llvm::dyn_cast(ND)) { std::string func_name; llvm::raw_string_ostream name_stream(func_name); FD->getNameForDiagnostic(name_stream, Policy, qualified); @@ -949,15 +924,15 @@ static std::string GetCompleteNameImpl(TCppType_t klass, bool qualified) { return ""; } -std::string GetCompleteName(TCppType_t klass) { - INTEROP_TRACE(klass); - return INTEROP_RETURN(GetCompleteNameImpl(klass, /*qualified=*/false)); +std::string GetCompleteName(ConstDeclRef DRef) { + INTEROP_TRACE(DRef); + return INTEROP_RETURN(GetCompleteNameImpl(DRef, /*qualified=*/false)); } -std::string GetQualifiedName(TCppType_t klass) { - INTEROP_TRACE(klass); - auto* D = (Decl*)klass; - if (auto* ND = llvm::dyn_cast_or_null(D)) { +std::string GetQualifiedName(ConstDeclRef DRef) { + INTEROP_TRACE(DRef); + const auto* D = unwrap(DRef); + if (const auto* ND = llvm::dyn_cast_or_null(D)) { return INTEROP_RETURN(ND->getQualifiedNameAsString()); } @@ -968,14 +943,14 @@ std::string GetQualifiedName(TCppType_t klass) { return INTEROP_RETURN(""); } -std::string GetQualifiedCompleteName(TCppType_t klass) { - INTEROP_TRACE(klass); - return INTEROP_RETURN(GetCompleteNameImpl(klass, /*qualified=*/true)); +std::string GetQualifiedCompleteName(ConstDeclRef DRef) { + INTEROP_TRACE(DRef); + return INTEROP_RETURN(GetCompleteNameImpl(DRef, /*qualified=*/true)); } -std::string GetDoxygenComment(TCppScope_t scope, bool strip_comment_markers) { - INTEROP_TRACE(scope, strip_comment_markers); - auto* D = static_cast(scope); +std::string GetDoxygenComment(ConstDeclRef DRef, bool strip_comment_markers) { + INTEROP_TRACE(DRef, strip_comment_markers); + const auto* D = unwrap(DRef); if (!D) return INTEROP_RETURN(""); @@ -996,22 +971,22 @@ std::string GetDoxygenComment(TCppScope_t scope, bool strip_comment_markers) { return INTEROP_RETURN(RC->getFormattedText(SM, C.getDiagnostics())); } -std::vector GetUsingNamespaces(TCppScope_t scope) { - INTEROP_TRACE(scope); - auto* D = (clang::Decl*)scope; +std::vector GetUsingNamespaces(ConstDeclRef DRef) { + INTEROP_TRACE(DRef); + const auto* D = unwrap(DRef); - if (auto* DC = llvm::dyn_cast_or_null(D)) { - std::vector namespaces; + if (const auto* DC = llvm::dyn_cast_or_null(D)) { + std::vector namespaces; for (auto UD : DC->using_directives()) { - namespaces.push_back((TCppScope_t)UD->getNominatedNamespace()); + namespaces.push_back(UD->getNominatedNamespace()); } return INTEROP_RETURN(namespaces); } - return INTEROP_RETURN(std::vector{}); + return INTEROP_RETURN(std::vector{}); } -TCppScope_t GetGlobalScope() { +DeclRef GetGlobalScope() { INTEROP_TRACE(); return INTEROP_RETURN( getSema().getASTContext().getTranslationUnitDecl()->getFirstDecl()); @@ -1030,32 +1005,36 @@ static Decl* GetScopeFromType(QualType QT) { return 0; } -TCppScope_t GetScopeFromType(TCppType_t type) { - INTEROP_TRACE(type); - QualType QT = QualType::getFromOpaquePtr(type); - return INTEROP_RETURN((TCppScope_t)GetScopeFromType(QT)); +DeclRef GetScopeFromType(ConstTypeRef TyRef) { + INTEROP_TRACE(TyRef); + QualType QT = QualType::getFromOpaquePtr(TyRef.data); + return INTEROP_RETURN(GetScopeFromType(QT)); } -static clang::Decl* GetUnderlyingScope(clang::Decl* D) { - if (auto* TND = dyn_cast_or_null(D)) { +static const clang::Decl* GetUnderlyingScopeImpl(const clang::Decl* D) { + if (const auto* TND = dyn_cast_or_null(D)) { if (auto* Scope = GetScopeFromType(TND->getUnderlyingType())) D = Scope; - } else if (auto* USS = dyn_cast_or_null(D)) { - if (auto* Scope = USS->getTargetDecl()) + } else if (const auto* USS = dyn_cast_or_null(D)) { + if (const auto* Scope = USS->getTargetDecl()) D = Scope; } return D->getCanonicalDecl(); } -TCppScope_t GetUnderlyingScope(TCppScope_t scope) { - INTEROP_TRACE(scope); - if (!scope) +DeclRef GetUnderlyingScope(ConstDeclRef DRef) { + INTEROP_TRACE(DRef); + if (!DRef) return INTEROP_RETURN(nullptr); - return INTEROP_RETURN(GetUnderlyingScope((clang::Decl*)scope)); + // Strip const at the API boundary: GetUnderlyingScope is a CONST + // operation but returns a mutable DRef (callers may use the result + // for further operations). + return INTEROP_RETURN(const_cast( + GetUnderlyingScopeImpl(unwrap(DRef)))); } -TCppScope_t GetScope(const std::string& name, TCppScope_t parent) { +DeclRef GetScope(const std::string& name, ConstDeclRef parent) { INTEROP_TRACE(name, parent); // FIXME: GetScope should be replaced by a general purpose lookup // and filter function. The function should be like GetNamed but @@ -1064,7 +1043,7 @@ TCppScope_t GetScope(const std::string& name, TCppScope_t parent) { if (name == "") return INTEROP_RETURN(GetGlobalScope()); - auto* ND = (NamedDecl*)GetNamed(name, parent); + auto* ND = unwrap(GetNamed(name, parent)); if (!ND || ND == (NamedDecl*)-1) return INTEROP_RETURN(nullptr); @@ -1072,17 +1051,17 @@ TCppScope_t GetScope(const std::string& name, TCppScope_t parent) { if (llvm::isa(ND) || llvm::isa(ND) || llvm::isa(ND) || llvm::isa(ND) || llvm::isa(ND) || llvm::isa(ND)) - return INTEROP_RETURN((TCppScope_t)(ND->getCanonicalDecl())); + return INTEROP_RETURN(ND->getCanonicalDecl()); return INTEROP_RETURN(nullptr); } -TCppScope_t GetScopeFromCompleteName(const std::string& name) { +DeclRef GetScopeFromCompleteName(const std::string& name) { INTEROP_TRACE(name); std::string delim = "::"; size_t start = 0; size_t end = name.find(delim); - TCppScope_t curr_scope = 0; + DeclRef curr_scope = nullptr; while (end != std::string::npos) { curr_scope = GetScope(name.substr(start, end - start), curr_scope); start = end + delim.length(); @@ -1113,7 +1092,7 @@ clang::Scope* BuildSyntheticScopeChain(clang::Sema& S, clang::DeclContext* DC) { return Mine; } -// RAII: build a synthetic scope chain for `Within`, install it as +// RAII: build a synthetic DRef chain for `Within`, install it as // Sema::CurScope, restore + delete on destruction. CppLookupName is // read-only on Scope/Sema state (only getParent/getEntity/ // getLookupEntity/isDeclScope reads, plus a stack-local @@ -1182,7 +1161,7 @@ clang::NamedDecl* LookupUnqualified(clang::Sema& S, } // Cheap probe: does any namespace from `DC` up to TU carry at least -// one using-directive? Gates the synthetic-scope-chain build below so +// one using-directive? Gates the synthetic-DRef-chain build below so // the common case (no using-directives anywhere on the path) doesn't // pay the heap-allocation tax. bool HasReachableUsingDirective(const clang::DeclContext* DC) { @@ -1197,13 +1176,11 @@ bool HasReachableUsingDirective(const clang::DeclContext* DC) { } } // namespace -TCppScope_t GetNamed(const std::string& name, - TCppScope_t parent /*= nullptr*/) { +DeclRef GetNamed(const std::string& name, ConstDeclRef parent /*= nullptr*/) { INTEROP_TRACE(name, parent); clang::DeclContext* Within = 0; if (parent) { - auto* D = (clang::Decl*)parent; - D = GetUnderlyingScope(D); + auto* D = unwrap(GetUnderlyingScope(parent)); Within = llvm::dyn_cast(D); } #ifdef CPPINTEROP_USE_CLING @@ -1212,32 +1189,35 @@ TCppScope_t GetNamed(const std::string& name, #endif compat::SynthesizingCodeRAII RAII(&getInterp()); - // Fast path: qualified lookup. Cheap, no scope-chain allocation, and + // Fast path: qualified lookup. Cheap, no DRef-chain allocation, and // resolves every name not brought into `Within` via a using-directive. // Lookup::Named falls back to LookupName(R, TUScope) when Within is // null, so TU-level using-directives are already handled there. auto* ND = CppInternal::utils::Lookup::Named(&getSema(), name, Within); if (ND && ND != (clang::NamedDecl*)-1) - return INTEROP_RETURN((TCppScope_t)(ND->getCanonicalDecl())); + return INTEROP_RETURN(ND->getCanonicalDecl()); // Slow path: only when qualified lookup missed AND `Within` is a // namespace whose enclosing chain carries at least one using-directive // (the only reason qualified-vs-unqualified disagree at namespace - // scope per [basic.lookup.unqual] vs [basic.lookup.qual]). + // DRef per [basic.lookup.unqual] vs [basic.lookup.qual]). if (!Within || !llvm::isa(Within) || !HasReachableUsingDirective(Within)) return INTEROP_RETURN(nullptr); clang::DeclarationName DName = &getSema().Context.Idents.get(name); ND = LookupUnqualified(getSema(), DName, Within); if (ND && ND != (clang::NamedDecl*)-1) - return INTEROP_RETURN((TCppScope_t)(ND->getCanonicalDecl())); + return INTEROP_RETURN(ND->getCanonicalDecl()); return INTEROP_RETURN(nullptr); } -TCppScope_t GetParentScope(TCppScope_t scope) { - INTEROP_TRACE(scope); - auto* D = (clang::Decl*)scope; +DeclRef GetParentScope(ConstDeclRef DRef) { + INTEROP_TRACE(DRef); + // const_cast: the returned DeclRef is a mutable DRef, so the caller may + // mutate the AST. Walking parents is logically const, but the return TyRef + // is the mutable DRef. + auto* D = const_cast(unwrap(DRef)); if (llvm::isa_and_nonnull(D)) { return INTEROP_RETURN(nullptr); @@ -1250,19 +1230,21 @@ TCppScope_t GetParentScope(TCppScope_t scope) { auto* P = clang::Decl::castFromDeclContext(ParentDC)->getCanonicalDecl(); if (auto* TU = llvm::dyn_cast_or_null(P)) - return INTEROP_RETURN((TCppScope_t)TU->getFirstDecl()); + return INTEROP_RETURN(TU->getFirstDecl()); - return INTEROP_RETURN((TCppScope_t)P); + return INTEROP_RETURN(P); } -TCppIndex_t GetNumBases(TCppScope_t klass) { - INTEROP_TRACE(klass); - auto* D = (Decl*)klass; +size_t GetNumBases(ConstDeclRef DRef) { + INTEROP_TRACE(DRef); + const auto* D = unwrap(DRef); - if (auto* CTSD = llvm::dyn_cast_or_null(D)) + if (const auto* CTSD = + llvm::dyn_cast_or_null(D)) if (!CTSD->hasDefinition()) - compat::InstantiateClassTemplateSpecialization(getInterp(), CTSD); - if (auto* CXXRD = llvm::dyn_cast_or_null(D)) { + compat::InstantiateClassTemplateSpecialization( + getInterp(), const_cast(CTSD)); + if (const auto* CXXRD = llvm::dyn_cast_or_null(D)) { if (CXXRD->hasDefinition()) return INTEROP_RETURN(CXXRD->getNumBases()); } @@ -1270,23 +1252,23 @@ TCppIndex_t GetNumBases(TCppScope_t klass) { return INTEROP_RETURN(0); } -TCppScope_t GetBaseClass(TCppScope_t klass, TCppIndex_t ibase) { - INTEROP_TRACE(klass, ibase); - auto* D = (Decl*)klass; - auto* CXXRD = llvm::dyn_cast_or_null(D); +DeclRef GetBaseClass(ConstDeclRef DRef, size_t ibase) { + INTEROP_TRACE(DRef, ibase); + const auto* D = unwrap(DRef); + const auto* CXXRD = llvm::dyn_cast_or_null(D); if (!CXXRD || CXXRD->getNumBases() <= ibase) return INTEROP_RETURN(nullptr); - auto type = (CXXRD->bases_begin() + ibase)->getType(); - if (auto RT = type->getAs()) - return INTEROP_RETURN((TCppScope_t)RT->getDecl()->getCanonicalDecl()); + auto TyRef = (CXXRD->bases_begin() + ibase)->getType(); + if (const auto* RT = TyRef->getAs()) + return INTEROP_RETURN(RT->getDecl()->getCanonicalDecl()); return INTEROP_RETURN(nullptr); } // FIXME: Consider dropping this interface as it seems the same as // IsTypeDerivedFrom. -bool IsSubclass(TCppScope_t derived, TCppScope_t base) { +bool IsSubclass(ConstDeclRef derived, ConstDeclRef base) { INTEROP_TRACE(derived, base); if (derived == base) return INTEROP_RETURN(true); @@ -1294,14 +1276,14 @@ bool IsSubclass(TCppScope_t derived, TCppScope_t base) { if (!derived || !base) return INTEROP_RETURN(false); - auto* derived_D = (clang::Decl*)derived; - auto* base_D = (clang::Decl*)base; + const auto* derived_D = unwrap(derived); + const auto* base_D = unwrap(base); if (!isa(derived_D) || !isa(base_D)) return INTEROP_RETURN(false); - auto Derived = cast(derived_D); - auto Base = cast(base_D); + const auto* Derived = cast(derived_D); + const auto* Base = cast(base_D); return INTEROP_RETURN( IsTypeDerivedFrom(GetTypeFromScope(Derived), GetTypeFromScope(Base))); } @@ -1354,19 +1336,19 @@ static unsigned ComputeBaseOffset(const ASTContext& Context, return NonVirtualOffset.getQuantity(); } -int64_t GetBaseClassOffset(TCppScope_t derived, TCppScope_t base) { +int64_t GetBaseClassOffset(ConstDeclRef derived, ConstDeclRef base) { INTEROP_TRACE(derived, base); if (base == derived) return INTEROP_RETURN(0); assert(derived || base); - auto* DD = (Decl*)derived; - auto* BD = (Decl*)base; + const auto* DD = unwrap(derived); + const auto* BD = unwrap(base); if (!isa(DD) || !isa(BD)) return INTEROP_RETURN(-1); - CXXRecordDecl* DCXXRD = cast(DD); - CXXRecordDecl* BCXXRD = cast(BD); + const auto* DCXXRD = cast(DD); + const auto* BCXXRD = cast(BD); // GCC's -Wmaybe-uninitialized false-positives here only under ASan: // -fsanitize=address keeps the SmallDenseMap's union storage live across // poison/unpoison calls and blocks the SROA pass that normally folds away @@ -1390,22 +1372,31 @@ int64_t GetBaseClassOffset(TCppScope_t derived, TCppScope_t base) { ComputeBaseOffset(getSema().getASTContext(), DCXXRD, Paths.front())); } -template -static void GetClassDecls(TCppScope_t klass, - std::vector& methods) { - if (!klass) +template +static void GetClassDecls(ConstDeclRef DRef, std::vector& methods) { + if (!DRef) return; - auto* D = (clang::Decl*)klass; + // Unwrap to mutable: ForceDeclarationOfImplicitMembers is a lazy-init + // operation on the AST, logically const for the caller. + Decl* D = const_cast(unwrap(DRef)); - if (auto* TD = dyn_cast(D)) - D = GetScopeFromType(TD->getUnderlyingType()); + if (auto* TD = dyn_cast(D)) { + DeclRef Scope = GetScopeFromType(TD->getUnderlyingType()); + D = unwrap(Scope); + } if (!D || !isa(D)) return; auto* CXXRD = dyn_cast(D); compat::SynthesizingCodeRAII RAII(&getInterp()); + if (auto* CTSD = dyn_cast(CXXRD)) { + QualType QT = compat::GetTypeFromDecl(CTSD); + if (!getSema().isCompleteType(CTSD->getLocation(), QT)) + return; // Unsuccesfull instantiaton + } + if (CXXRD->hasDefinition()) CXXRD = CXXRD->getDefinition(); getSema().ForceDeclarationOfImplicitMembers(CXXRD); @@ -1439,47 +1430,47 @@ static void GetClassDecls(TCppScope_t klass, } } -void GetClassMethods(TCppScope_t klass, std::vector& methods) { - INTEROP_TRACE(klass, INTEROP_OUT(methods)); - GetClassDecls(klass, methods); +void GetClassMethods(ConstDeclRef DRef, std::vector& methods) { + INTEROP_TRACE(DRef, INTEROP_OUT(methods)); + GetClassDecls(DRef, methods); return INTEROP_VOID_RETURN(); } -void GetFunctionTemplatedDecls(TCppScope_t klass, - std::vector& methods) { - INTEROP_TRACE(klass, INTEROP_OUT(methods)); - GetClassDecls(klass, methods); +void GetFunctionTemplatedDecls(ConstDeclRef DRef, + std::vector& methods) { + INTEROP_TRACE(DRef, INTEROP_OUT(methods)); + GetClassDecls(DRef, methods); return INTEROP_VOID_RETURN(); } -bool HasDefaultConstructor(TCppScope_t scope) { - INTEROP_TRACE(scope); - auto* D = (clang::Decl*)scope; +bool HasDefaultConstructor(ConstDeclRef DRef) { + INTEROP_TRACE(DRef); + const auto* D = unwrap(DRef); - if (auto* CXXRD = llvm::dyn_cast_or_null(D)) + if (const auto* CXXRD = llvm::dyn_cast_or_null(D)) return INTEROP_RETURN(CXXRD->hasDefaultConstructor()); return INTEROP_RETURN(false); } -TCppFunction_t GetDefaultConstructor(compat::Interpreter& interp, - TCppScope_t scope) { - if (!HasDefaultConstructor(scope)) +FuncRef GetDefaultConstructor(compat::Interpreter& interp, DeclRef DRef) { + if (!HasDefaultConstructor(DRef)) return nullptr; - auto* CXXRD = (clang::CXXRecordDecl*)scope; + auto* CXXRD = unwrap(DRef); compat::SynthesizingCodeRAII RAII(&getInterp()); return interp.getCI()->getSema().LookupDefaultConstructor(CXXRD); } -TCppFunction_t GetDefaultConstructor(TCppScope_t scope) { - INTEROP_TRACE(scope); - return INTEROP_RETURN(GetDefaultConstructor(getInterp(), scope)); +FuncRef GetDefaultConstructor(DeclRef DRef) { + INTEROP_TRACE(DRef); + return INTEROP_RETURN(GetDefaultConstructor(getInterp(), DRef)); } -TCppFunction_t GetDestructor(TCppScope_t scope) { - INTEROP_TRACE(scope); - auto* D = (clang::Decl*)scope; +FuncRef GetDestructor(ConstDeclRef DRef) { + INTEROP_TRACE(DRef); + // ForceDeclarationOfImplicitMembers is a lazy-init operation. + auto* D = const_cast(unwrap(DRef)); if (auto* CXXRD = llvm::dyn_cast_or_null(D)) { getSema().ForceDeclarationOfImplicitMembers(CXXRD); @@ -1489,27 +1480,53 @@ TCppFunction_t GetDestructor(TCppScope_t scope) { return INTEROP_RETURN(nullptr); } -void DumpScope(TCppScope_t scope) { - INTEROP_TRACE(scope); - auto* D = (clang::Decl*)scope; +void DumpScope(ConstDeclRef DRef) { + INTEROP_TRACE(DRef); + const auto* D = unwrap(DRef); D->dump(); return INTEROP_VOID_RETURN(); } -std::vector GetFunctionsUsingName(TCppScope_t scope, - const std::string& name) { - INTEROP_TRACE(scope, name); - auto* D = (Decl*)scope; +// Map an operator spelling (e.g. "operator==") to the CXXOperatorName its +// overloads are stored under, since identifier lookup never matches them. +// Returns an empty DeclarationName for non-operators (e.g. "operators_count"), +// leaving the caller on the identifier path. We can't use LookupOperatorName +// for this: it ignores class members, which this entry point must also find. +static DeclarationName getCXXOperatorDeclName(ASTContext& Ctx, + llvm::StringRef name) { + static constexpr llvm::StringRef OperatorPrefix("operator"); + if (!name.consume_front(OperatorPrefix) || name.empty() || + clang::isAsciiIdentifierContinue( + static_cast(name.front()))) + return DeclarationName(); + + llvm::StringRef Spelling = name.trim(); +#define OVERLOADED_OPERATOR(OpName, OpSpelling, Token, Unary, Binary, \ + MemberOnly) \ + if (Spelling == (OpSpelling)) \ + return Ctx.DeclarationNames.getCXXOperatorName(clang::OO_##OpName); +#include "clang/Basic/OperatorKinds.def" +#undef OVERLOADED_OPERATOR + return DeclarationName(); +} - if (!scope || name.empty()) - return INTEROP_RETURN(std::vector{}); +std::vector GetFunctionsUsingName(ConstDeclRef DRef, + const std::string& name) { + INTEROP_TRACE(DRef, name); - D = GetUnderlyingScope(D); + if (!DRef || name.empty()) + return INTEROP_RETURN(std::vector{}); - std::vector funcs; - llvm::StringRef Name(name); + const auto* D = unwrap(GetUnderlyingScope(DRef)); + + std::vector funcs; auto& S = getSema(); - DeclarationName DName = &getASTContext().Idents.get(name); + auto& Ctx = getASTContext(); + + DeclarationName DName = getCXXOperatorDeclName(Ctx, name); + if (!DName) + DName = &Ctx.Idents.get(name); + clang::LookupResult R(S, DName, SourceLocation(), Sema::LookupOrdinaryName, RedeclarationKind::ForVisibleRedeclaration); @@ -1520,52 +1537,59 @@ std::vector GetFunctionsUsingName(TCppScope_t scope, R.resolveKind(); - for (auto* Found : R) + for (auto* Found : R) { if (llvm::isa(Found)) funcs.push_back(Found); + else if (auto* USD = llvm::dyn_cast(Found)) { + if (auto* FTD = llvm::dyn_cast(USD->getTargetDecl())) + funcs.push_back(FTD); + } + } return INTEROP_RETURN(funcs); } -TCppType_t GetFunctionReturnType(TCppFunction_t func) { +TypeRef GetFunctionReturnType(ConstFuncRef func) { INTEROP_TRACE(func); - auto* D = (clang::Decl*)func; - if (auto* FD = llvm::dyn_cast_or_null(D)) { + const auto* D = unwrap(func); + if (const auto* FD = llvm::dyn_cast_or_null(D)) { QualType Type = FD->getReturnType(); if (Type->isUndeducedAutoType()) { bool needInstantiation = false; if (IsTemplatedFunction(FD) && !FD->isDefined()) needInstantiation = true; - if (auto* MD = llvm::dyn_cast(FD)) { + if (const auto* MD = llvm::dyn_cast(FD)) { if (IsTemplateSpecialization(MD->getParent())) needInstantiation = true; } if (needInstantiation) { - InstantiateFunctionDefinition(FD); + // Lazy AST instantiation — logically const for the caller. + InstantiateFunctionDefinition( + const_cast(static_cast(FD))); } Type = FD->getReturnType(); } return INTEROP_RETURN(Type.getAsOpaquePtr()); } - if (auto* FD = llvm::dyn_cast_or_null(D)) + if (const auto* FD = llvm::dyn_cast_or_null(D)) return INTEROP_RETURN( (FD->getTemplatedDecl())->getReturnType().getAsOpaquePtr()); return INTEROP_RETURN(nullptr); } -bool IsFunctionProtoType(TCppType_t typ) { - INTEROP_TRACE(typ); - QualType QT = QualType::getFromOpaquePtr(typ); +bool IsFunctionProtoType(ConstTypeRef TyRef) { + INTEROP_TRACE(TyRef); + QualType QT = QualType::getFromOpaquePtr(TyRef.data); const auto* T = QT.getTypePtr(); return llvm::isa_and_nonnull(T); } -void GetFnTypeSignature(TCppType_t fn_type, std::vector& sig) { +void GetFnTypeSignature(ConstTypeRef fn_type, std::vector& sig) { INTEROP_TRACE(fn_type, INTEROP_OUT(sig)); - QualType QT = QualType::getFromOpaquePtr(fn_type); + QualType QT = QualType::getFromOpaquePtr(fn_type.data); const auto* FPT = QT->getAs(); if (!FPT) return INTEROP_VOID_RETURN(); @@ -1575,37 +1599,46 @@ void GetFnTypeSignature(TCppType_t fn_type, std::vector& sig) { return INTEROP_VOID_RETURN(); } -TCppIndex_t GetFunctionNumArgs(TCppFunction_t func) { +// A C++23 explicit object parameter (the `this Self self` of a "deducing this" +// member function) is a real ParmVarDecl, but it binds to the object the method +// is invoked on rather than being callee-supplied. Clang's getNumParams() / +// getMinRequiredArguments() count it; the *NonObject* / *Explicit* variants +// exclude it, which is what callers introspecting the argument list want. +size_t GetFunctionNumArgs(ConstFuncRef func) { INTEROP_TRACE(func); - auto* D = (clang::Decl*)func; - if (auto* FD = llvm::dyn_cast_or_null(D)) - return INTEROP_RETURN(FD->getNumParams()); + const auto* D = unwrap(func); + if (const auto* FD = llvm::dyn_cast_or_null(D)) + return INTEROP_RETURN(FD->getNumNonObjectParams()); - if (auto* FD = llvm::dyn_cast_or_null(D)) - return INTEROP_RETURN((FD->getTemplatedDecl())->getNumParams()); + if (const auto* FD = llvm::dyn_cast_or_null(D)) + return INTEROP_RETURN(FD->getTemplatedDecl()->getNumNonObjectParams()); return INTEROP_RETURN(0); } -TCppIndex_t GetFunctionRequiredArgs(TCppConstFunction_t func) { +size_t GetFunctionRequiredArgs(ConstFuncRef func) { INTEROP_TRACE(func); - const auto* D = static_cast(func); - if (auto* FD = llvm::dyn_cast_or_null(D)) - return INTEROP_RETURN(FD->getMinRequiredArguments()); + const auto* D = unwrap(func); + if (const auto* FD = llvm::dyn_cast_or_null(D)) + return INTEROP_RETURN(FD->getMinRequiredExplicitArguments()); - if (auto* FD = llvm::dyn_cast_or_null(D)) - return INTEROP_RETURN((FD->getTemplatedDecl())->getMinRequiredArguments()); + if (const auto* FD = llvm::dyn_cast_or_null(D)) + return INTEROP_RETURN( + FD->getTemplatedDecl()->getMinRequiredExplicitArguments()); return INTEROP_RETURN(0); } -TCppType_t GetFunctionArgType(TCppFunction_t func, TCppIndex_t iarg) { +TypeRef GetFunctionArgType(ConstFuncRef func, size_t iarg) { INTEROP_TRACE(func, iarg); - auto* D = (clang::Decl*)func; + const auto* D = unwrap(func); + + if (const auto* FTD = llvm::dyn_cast_or_null(D)) + D = FTD->getTemplatedDecl(); - if (auto* FD = llvm::dyn_cast_or_null(D)) { - if (iarg < FD->getNumParams()) { - auto* PVD = FD->getParamDecl(iarg); + if (const auto* FD = llvm::dyn_cast_or_null(D)) { + if (iarg < FD->getNumNonObjectParams()) { + const auto* PVD = FD->getNonObjectParameter(iarg); return INTEROP_RETURN(PVD->getOriginalType().getAsOpaquePtr()); } } @@ -1613,17 +1646,23 @@ TCppType_t GetFunctionArgType(TCppFunction_t func, TCppIndex_t iarg) { return INTEROP_RETURN(nullptr); } -std::string GetFunctionSignature(TCppFunction_t func) { +bool IsTemplateParmType(ConstTypeRef TyRef) { + INTEROP_TRACE(TyRef); + clang::QualType QT = clang::QualType::getFromOpaquePtr(TyRef.data); + return INTEROP_RETURN(QT->isTemplateTypeParmType()); +} + +std::string GetFunctionSignature(ConstFuncRef func) { INTEROP_TRACE(func); if (!func) return INTEROP_RETURN(""); - auto* D = (clang::Decl*)func; - clang::FunctionDecl* FD; + const auto* D = unwrap(func); + const clang::FunctionDecl* FD; if (llvm::dyn_cast(D)) FD = llvm::dyn_cast(D); - else if (auto* FTD = llvm::dyn_cast(D)) + else if (const auto* FTD = llvm::dyn_cast(D)) FD = FTD->getTemplatedDecl(); else return INTEROP_RETURN(""); @@ -1643,12 +1682,12 @@ std::string GetFunctionSignature(TCppFunction_t func) { // Internal functions that are not needed outside the library are // encompassed in an anonymous namespace as follows. namespace { -bool IsTemplatedFunction(Decl* D) { +bool IsTemplatedFunction(const Decl* D) { return llvm::isa_and_nonnull(D); } -bool IsTemplateInstantiationOrSpecialization(Decl* D) { - if (auto* FD = llvm::dyn_cast_or_null(D)) { +bool IsTemplateInstantiationOrSpecialization(const Decl* D) { + if (const auto* FD = llvm::dyn_cast_or_null(D)) { auto TK = FD->getTemplatedKind(); return TK == FunctionDecl::TemplatedKind::TK_FunctionTemplateSpecialization || @@ -1661,16 +1700,15 @@ bool IsTemplateInstantiationOrSpecialization(Decl* D) { } } // namespace -bool IsFunctionDeleted(TCppConstFunction_t function) { +bool IsFunctionDeleted(ConstFuncRef function) { INTEROP_TRACE(function); - const auto* FD = - cast(static_cast(function)); + const auto* FD = cast(unwrap(function)); return INTEROP_RETURN(FD->isDeleted()); } -bool IsTemplatedFunction(TCppFunction_t func) { +bool IsTemplatedFunction(ConstFuncRef func) { INTEROP_TRACE(func); - auto* D = (Decl*)func; + const auto* D = unwrap(func); return INTEROP_RETURN(IsTemplatedFunction(D) || IsTemplateInstantiationOrSpecialization(D)); } @@ -1678,11 +1716,11 @@ bool IsTemplatedFunction(TCppFunction_t func) { // FIXME: This lookup is broken, and should no longer be used in favour of // `GetClassTemplatedMethods` If the candidate set returned is =1, that means // the template function exists and >1 means overloads -bool ExistsFunctionTemplate(const std::string& name, TCppScope_t parent) { +bool ExistsFunctionTemplate(const std::string& name, ConstDeclRef parent) { INTEROP_TRACE(name, parent); - DeclContext* Within = 0; + const DeclContext* Within = nullptr; if (parent) { - auto* D = (Decl*)parent; + const auto* D = unwrap(parent); Within = llvm::dyn_cast(D); } @@ -1700,40 +1738,41 @@ bool ExistsFunctionTemplate(const std::string& name, TCppScope_t parent) { } // Looks up all constructors in the current DeclContext -void LookupConstructors(const std::string& name, TCppScope_t parent, - std::vector& funcs) { +void LookupConstructors(const std::string& name, ConstDeclRef parent, + std::vector& funcs) { INTEROP_TRACE(name, parent, INTEROP_OUT(funcs)); - auto* D = (Decl*)parent; + // ForceDeclarationOfImplicitMembers / LookupConstructors are lazy-init ops. + auto* D = const_cast(unwrap(parent)); if (auto* CXXRD = llvm::dyn_cast_or_null(D)) { getSema().ForceDeclarationOfImplicitMembers(CXXRD); DeclContextLookupResult Result = getSema().LookupConstructors(CXXRD); // Obtaining all constructors when we intend to lookup a method under a - // scope can lead to crashes. We avoid that by accumulating constructors + // DRef can lead to crashes. We avoid that by accumulating constructors // only if the Decl matches the lookup name. for (auto* i : Result) - if (GetName(i) == name) + if (GetName(DeclRef(i)) == name) funcs.push_back(i); } return INTEROP_VOID_RETURN(); } -bool GetClassTemplatedMethods(const std::string& name, TCppScope_t parent, - std::vector& funcs) { +bool GetClassTemplatedMethods(const std::string& name, ConstDeclRef parent, + std::vector& funcs) { INTEROP_TRACE(name, parent, INTEROP_OUT(funcs)); - auto* D = (Decl*)parent; + const auto* D = unwrap(parent); if (!D && name.empty()) return INTEROP_RETURN(false); // Accumulate constructors LookupConstructors(name, parent, funcs); auto& S = getSema(); - D = GetUnderlyingScope(D); + auto* DU = unwrap(GetUnderlyingScope(parent)); llvm::StringRef Name(name); DeclarationName DName = &getASTContext().Idents.get(name); clang::LookupResult R(S, DName, SourceLocation(), Sema::LookupOrdinaryName, RedeclarationKind::ForVisibleRedeclaration); - auto* DC = clang::Decl::castToDeclContext(D); + auto* DC = clang::Decl::castToDeclContext(DU); CppInternal::utils::Lookup::Named(&S, R, DC); if (R.getResultKind() == clang_LookupResult_Not_Found && funcs.empty()) @@ -1746,9 +1785,15 @@ bool GetClassTemplatedMethods(const std::string& name, TCppScope_t parent, } // Loop over overload set else if (R.getResultKind() == clang_LookupResult_Found_Overloaded) { - for (auto* Found : R) + for (auto* Found : R) { if (IsTemplatedFunction(Found)) funcs.push_back(Found); + else if (auto* USD = llvm::dyn_cast(Found)) { + if (auto* FTD = + llvm::dyn_cast(USD->getTargetDecl())) + funcs.push_back(FTD); + } + } } // TODO: Handle ambiguously found LookupResult @@ -1762,8 +1807,8 @@ bool GetClassTemplatedMethods(const std::string& name, TCppScope_t parent, } // Adapted from inner workings of Sema::BuildCallExpr -TCppFunction_t -BestOverloadFunctionMatch(const std::vector& candidates, +FuncRef +BestOverloadFunctionMatch(const std::vector& candidates, const std::vector& explicit_types, const std::vector& arg_types) { INTEROP_TRACE(candidates, explicit_types, arg_types); @@ -1804,7 +1849,7 @@ BestOverloadFunctionMatch(const std::vector& candidates, for (auto explicit_type : explicit_types) { QualType ArgTy = QualType::getFromOpaquePtr(explicit_type.m_Type); if (explicit_type.m_IntegralValue) { - // We have a non-type template parameter. Create an integral value from + // We have a non-TyRef template parameter. Create an integral value from // the string representation. auto Res = llvm::APSInt(explicit_type.m_IntegralValue); Res = Res.extOrTrunc(C.getIntWidth(ArgTy)); @@ -1819,28 +1864,46 @@ BestOverloadFunctionMatch(const std::vector& candidates, ExplicitTemplateArgs.addArgument( S.getTrivialTemplateArgumentLoc(TA, QualType(), SourceLocation())); + // ensure valid point of instantiation, SFINAE trap keeps any failure soft + SourceLocation Loc = SourceLocation::getFromRawEncoding(1); + Sema::SFINAETrap Trap(S, /*ForValidityCheck=*/true); + OverloadCandidateSet Overloads( - SourceLocation(), OverloadCandidateSet::CandidateSetKind::CSK_Normal); + Loc, OverloadCandidateSet::CandidateSetKind::CSK_Normal); - for (void* i : candidates) { - Decl* D = static_cast(i); + for (auto i : candidates) { + auto* D = const_cast(unwrap(i)); if (auto* FD = dyn_cast(D)) { S.AddOverloadCandidate(FD, DeclAccessPair::make(FD, FD->getAccess()), Args, Overloads); } else if (auto* FTD = dyn_cast(D)) { - // AddTemplateOverloadCandidate is causing a memory leak - // It is a known bug at clang - // call stack: AddTemplateOverloadCandidate -> MakeDeductionFailureInfo - // source: - // https://github.com/llvm/llvm-project/blob/release/19.x/clang/lib/Sema/SemaOverload.cpp#L731-L756 - S.AddTemplateOverloadCandidate( - FTD, DeclAccessPair::make(FTD, FTD->getAccess()), - &ExplicitTemplateArgs, Args, Overloads); + auto* MD = dyn_cast(FTD->getTemplatedDecl()); + if (MD && MD->isExplicitObjectMemberFunction()) { + // The explicit object parameter isn't in Args, so deduce it via the + // method-template path with a synthesized receiver of the record type. + CXXRecordDecl* RD = MD->getParent(); + QualType ObjectType = compat::GetTypeFromDecl(RD); + OpaqueValueExpr ObjectExpr(SourceLocation::getFromRawEncoding(1), + ObjectType, ExprValueKind::VK_LValue); + S.AddMethodTemplateCandidate( + FTD, DeclAccessPair::make(FTD, FTD->getAccess()), RD, + &ExplicitTemplateArgs, ObjectType, ObjectExpr.Classify(C), Args, + Overloads); + } else { + // AddTemplateOverloadCandidate is causing a memory leak + // It is a known bug at clang + // call stack: AddTemplateOverloadCandidate -> MakeDeductionFailureInfo + // source: + // https://github.com/llvm/llvm-project/blob/release/19.x/clang/lib/Sema/SemaOverload.cpp#L731-L756 + S.AddTemplateOverloadCandidate( + FTD, DeclAccessPair::make(FTD, FTD->getAccess()), + &ExplicitTemplateArgs, Args, Overloads); + } } } OverloadCandidateSet::iterator Best; - Overloads.BestViableFunction(S, SourceLocation(), Best); + Overloads.BestViableFunction(S, Loc, Best); FunctionDecl* Result = Best != Overloads.end() ? Best->Function : nullptr; delete[] Exprs; @@ -1849,70 +1912,72 @@ BestOverloadFunctionMatch(const std::vector& candidates, // Gets the AccessSpecifier of the function and checks if it is equal to // the provided AccessSpecifier. -bool CheckMethodAccess(TCppFunction_t method, AccessSpecifier AS) { - auto* D = (Decl*)method; - if (auto* CXXMD = llvm::dyn_cast_or_null(D)) { +bool CheckMethodAccess(ConstFuncRef method, AccessSpecifier AS) { + const auto* D = unwrap(method); + if (const auto* CXXMD = llvm::dyn_cast_or_null(D)) { return CXXMD->getAccess() == AS; } return false; } -bool IsMethod(TCppConstFunction_t method) { +bool IsMethod(ConstFuncRef method) { INTEROP_TRACE(method); - return INTEROP_RETURN( - dyn_cast_or_null(static_cast(method))); + const auto* D = unwrap(method); + if (const auto* FTD = dyn_cast_or_null(D)) + D = FTD->getTemplatedDecl(); + return INTEROP_RETURN(dyn_cast_or_null(D)); } -bool IsPublicMethod(TCppFunction_t method) { +bool IsPublicMethod(ConstFuncRef method) { INTEROP_TRACE(method); return INTEROP_RETURN(CheckMethodAccess(method, AccessSpecifier::AS_public)); } -bool IsProtectedMethod(TCppFunction_t method) { +bool IsProtectedMethod(ConstFuncRef method) { INTEROP_TRACE(method); return INTEROP_RETURN( CheckMethodAccess(method, AccessSpecifier::AS_protected)); } -bool IsPrivateMethod(TCppFunction_t method) { +bool IsPrivateMethod(ConstFuncRef method) { INTEROP_TRACE(method); return INTEROP_RETURN(CheckMethodAccess(method, AccessSpecifier::AS_private)); } -bool IsConstructor(TCppConstFunction_t method) { +bool IsConstructor(ConstFuncRef method) { INTEROP_TRACE(method); - const auto* D = static_cast(method); + const auto* D = unwrap(method); if (const auto* FTD = dyn_cast(D)) return INTEROP_RETURN(IsConstructor(FTD->getTemplatedDecl())); return INTEROP_RETURN(llvm::isa_and_nonnull(D)); } -bool IsDestructor(TCppConstFunction_t method) { +bool IsDestructor(ConstFuncRef method) { INTEROP_TRACE(method); - const auto* D = static_cast(method); + const auto* D = unwrap(method); return INTEROP_RETURN(llvm::isa_and_nonnull(D)); } -bool IsStaticMethod(TCppConstFunction_t method) { +bool IsStaticMethod(ConstFuncRef method) { INTEROP_TRACE(method); - const auto* D = static_cast(method); + const auto* D = unwrap(method); if (const auto* FTD = llvm::dyn_cast_or_null(D)) D = FTD->getTemplatedDecl(); - if (auto* CXXMD = llvm::dyn_cast_or_null(D)) { + if (const auto* CXXMD = llvm::dyn_cast_or_null(D)) { return INTEROP_RETURN(CXXMD->isStatic()); } return INTEROP_RETURN(false); } -bool IsExplicit(TCppConstFunction_t method) { +bool IsExplicit(ConstFuncRef method) { INTEROP_TRACE(method); if (!method) return INTEROP_RETURN(false); - const auto* D = static_cast(method); + const auto* D = unwrap(method); if (const auto* FTD = llvm::dyn_cast_or_null(D)) D = FTD->getTemplatedDecl(); @@ -1929,7 +1994,7 @@ bool IsExplicit(TCppConstFunction_t method) { return INTEROP_RETURN(false); } -TCppFuncAddr_t GetFunctionAddress(const char* mangled_name) { +void* GetFunctionAddress(const char* mangled_name) { INTEROP_TRACE(mangled_name); auto& I = getInterp(); auto FDAorErr = compat::getSymbolAddress(I, mangled_name); @@ -1941,7 +2006,7 @@ TCppFuncAddr_t GetFunctionAddress(const char* mangled_name) { return INTEROP_RETURN(nullptr); } -static TCppFuncAddr_t GetFunctionAddress(const FunctionDecl* FD) { +static void* GetFunctionAddress(const FunctionDecl* FD) { const auto get_mangled_name = [](const FunctionDecl* FD) { auto MangleCtxt = getASTContext().createMangleContext(); @@ -1967,9 +2032,9 @@ static TCppFuncAddr_t GetFunctionAddress(const FunctionDecl* FD) { return 0; } -TCppFuncAddr_t GetFunctionAddress(TCppFunction_t method) { +void* GetFunctionAddress(FuncRef method) { INTEROP_TRACE(method); - auto* D = static_cast(method); + auto* D = unwrap(method); if (auto* FD = llvm::dyn_cast_or_null(D)) { if ((IsTemplateInstantiationOrSpecialization(FD) || FD->getTemplatedKind() == FunctionDecl::TK_MemberSpecialization) && @@ -1983,19 +2048,301 @@ TCppFuncAddr_t GetFunctionAddress(TCppFunction_t method) { return INTEROP_RETURN(nullptr); } -bool IsVirtualMethod(TCppFunction_t method) { +bool IsVirtualMethod(ConstFuncRef method) { INTEROP_TRACE(method); - auto* D = (Decl*)method; - if (auto* CXXMD = llvm::dyn_cast_or_null(D)) { + const auto* D = unwrap(method); + if (const auto* CXXMD = llvm::dyn_cast_or_null(D)) { return INTEROP_RETURN(CXXMD->isVirtual()); } return INTEROP_RETURN(false); } -void GetDatamembers(TCppScope_t scope, std::vector& datamembers) { - INTEROP_TRACE(scope, INTEROP_OUT(datamembers)); - auto* D = (Decl*)scope; +// Vtable slot index of a virtual method in the target ABI's layout, or -1 +// if the method is not virtual. Clang's VTableContext yields the right index +// for both Itanium (first user virtual after the destructor pair) and +// Microsoft (single deleting-dtor slot). +static int virtualMethodSlot(ConstFuncRef method) { + const auto* MD = llvm::dyn_cast_or_null(unwrap(method)); + if (!MD || !MD->isVirtual()) + return -1; + + ASTContext& C = getASTContext(); + if (C.getTargetInfo().getCXXABI().isMicrosoft()) { + auto* VTC = llvm::cast(C.getVTableContext()); + return (int)VTC->getMethodVFTableLocation(GlobalDecl(MD)).Index; + } + auto* VTC = llvm::cast(C.getVTableContext()); + return (int)VTC->getMethodVTableIndex(GlobalDecl(MD)); +} + +// Number of vtable slots from the address point onward for a polymorphic +// class (the count applyVTableOverlay copies): destructor slots plus every +// virtual. -1 if DRef is not a polymorphic class. +static int vtableMethodSlotCount(ConstDeclRef DRef) { + const auto* RD = llvm::dyn_cast_or_null(unwrap(DRef)); + if (RD) + RD = RD->getDefinition(); + if (!RD || !RD->isPolymorphic()) + return -1; + + ASTContext& C = getASTContext(); + if (C.getTargetInfo().getCXXABI().isMicrosoft()) { + auto* VTC = llvm::cast(C.getVTableContext()); + const VTableLayout& L = VTC->getVFTableLayout(RD, CharUnits::Zero()); + return (int)L.vtable_components().size(); + } + auto* VTC = llvm::cast(C.getVTableContext()); + const VTableLayout& L = VTC->getVTableLayout(RD); + unsigned AddrPoint = L.getAddressPointIndices()[0]; + return (int)(L.vtable_components().size() - AddrPoint); +} + +// True if \c RD's vtable layout is beyond the single-vptr overlay model: +// * multiple polymorphic direct bases -> secondary-base subobjects have +// their own vptrs that the overlay does not touch, so dispatch through +// a pointer to such a subobject would silently hit the original method; +// * any virtual base -> the primary vtable has vbase-offset entries +// before the address point, and the virtual-base subobject carries a +// vtable-in-derived with virtual thunks that the overlay does not +// retarget. Returns true so MakeVTableOverlay can refuse instead of +// quietly mis-overlaying. +static bool hasComplexVTableLayout(const CXXRecordDecl* RD) { + if (RD->getNumVBases() > 0) + return true; + unsigned polymorphic_direct_bases = 0; + for (const auto& B : RD->bases()) { + const auto* BD = B.getType()->getAsCXXRecordDecl(); + if (!BD) + continue; + BD = BD->getDefinition(); + if (BD && BD->isPolymorphic() && ++polymorphic_direct_bases > 1) + return true; + } + return false; +} + +// ABI-only prefix size (excludes the hidden self-pointer slot CppInterOp +// interposes for dtor-hook routing). Itanium has 2 slots (offset-to-top, +// type_info); MSVC has 1 (complete-object-locator). +#ifdef _WIN32 +constexpr int kABIPrefixSize = 1; +#else +constexpr int kABIPrefixSize = 2; +#endif +static_assert(detail::kVTableOverlayPrefixSize == kABIPrefixSize + 1, + "public prefix size must equal ABI prefix + 1 hidden 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: +// +// [ user extras (N) ] [ hidden self-ptr ] [ ABI prefix ] [ methods ] +// ^ ^ +// address_point - kVTableOverlayPrefixSize +// 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. +struct VTableOverlay { + void** block; // owned, freed in ~VTableOverlay + void** original_vptr; // restored on caller-driven teardown + void* inst; // object whose vptr was replaced + std::size_t n_extra_prefix_slots; + 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; + VTableOverlayDtorHook cleanup = nullptr; + void* cleanup_data = nullptr; + + VTableOverlay(void** block, void** orig_vptr, void* inst, + std::size_t n_extra) + : block(block), original_vptr(orig_vptr), inst(inst), + n_extra_prefix_slots(n_extra) { + // Stash self-pointer in the hidden slot before publishing the vptr; + // the wrapper reads it at fire time via a fixed offset from vptr. + *hidden_slot() = this; + WriteVPtr(inst, address_point()); + } + ~VTableOverlay() { + if (!dtor_fired) + WriteVPtr(inst, original_vptr); + delete[] block; + } + VTableOverlay(const VTableOverlay&) = delete; + VTableOverlay& operator=(const VTableOverlay&) = delete; + + void** address_point() const { + return block + n_extra_prefix_slots + detail::kVTableOverlayPrefixSize; + } + VTableOverlay** hidden_slot() const { + return reinterpret_cast(block + n_extra_prefix_slots); + } + + // reinterpret_cast between function and void* is only conditionally + // supported per [expr.reinterpret.cast]/6; memcpy is the well-defined + // alternative on every platform CppInterOp targets. + template static To BitCastFn(From f) noexcept { + static_assert(sizeof(To) == sizeof(From)); + To to; + std::memcpy(&to, &f, sizeof(to)); + return to; + } + + static void** ReadVPtr(void* inst) { + return *reinterpret_cast(inst); + } + static void WriteVPtr(void* inst, void** new_vptr) { + *reinterpret_cast(inst) = new_vptr; + } +}; + +// 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 +// 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); + VTableOverlay* ov = *reinterpret_cast( + 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); +} + +// Minimum slot count a polymorphic class can have from its address point: +// Itanium emits the destructor pair (D1 + D0) so the count is at least 2; +// Microsoft emits a single deleting-dtor slot so the count is at least 1. +// vtableMethodSlotCount returns -1 for non-polymorphic scopes. +#ifdef _WIN32 +constexpr int kMinVTableMethodSlots = 1; +#else +constexpr int kMinVTableMethodSlots = 2; +#endif + +// Reflection-free pointer surgery: copy inst's vtable, overwrite slots with +// fns, install the copy and return a DRef owning it. The slot indices and +// count are resolved from reflection by the caller (MakeVTableOverlay). +// n_extra_prefix_slots prepends nullptr-initialized void* slots before the +// ABI prefix; the caller stashes per-instance data there and thunks read it +// via vptr[-(kPrefix + 1 + i)]. +static VTableOverlay* applyVTableOverlay(void* inst, int total_method_slots, + const int* slots, void* const* fns, + std::size_t n, + std::size_t n_extra_prefix_slots) { + if (!inst || total_method_slots < kMinVTableMethodSlots) + return nullptr; + for (std::size_t i = 0; i < n; ++i) { + if (slots[i] < 0 || slots[i] >= total_method_slots) + return nullptr; + } + + // Total block size = N user extras + 1 hidden self-ptr + ABI prefix + // + total_method_slots. The published vtable's address point is at + // block + n_extra_prefix_slots + detail::kVTableOverlayPrefixSize. + constexpr int kPrefix = detail::kVTableOverlayPrefixSize; + const std::size_t total = n_extra_prefix_slots + kPrefix + total_method_slots; + + void** orig_vptr = VTableOverlay::ReadVPtr(inst); + // Zero-init so user-extra slots are nullptr (callers will populate). + // The hidden slot at block[n_extra_prefix_slots] is filled by the + // VTableOverlay ctor below. The memcpy then copies the original ABI + // prefix + methods into the remaining region. + void** block = new void*[total](); + std::memcpy(block + n_extra_prefix_slots + 1, orig_vptr - kABIPrefixSize, + (kABIPrefixSize + total_method_slots) * sizeof(void*)); + + for (std::size_t i = 0; i < n; ++i) + block[n_extra_prefix_slots + kPrefix + slots[i]] = fns[i]; + + // Per-instance install: the new vptr is written into *this* object only. + // Other live and future instances of the same TyRef continue to use the + // class's original vtable; ~VTableOverlay restores `inst`'s vptr. + return new VTableOverlay(block, orig_vptr, inst, n_extra_prefix_slots); +} + +// Itanium emits the destructor pair (D1, D0) at slots 0 and 1; the +// deleting-dtor (D0) at slot 1 is the path operator-delete takes for +// heap-allocated objects -- the relevant hook for cppyy / binding proxies +// whose Python wrapper drop triggers `delete cppobj`. MSVC emits a single +// deleting dtor at slot 0. +#ifdef _WIN32 +constexpr int kDeletingDtorSlot = 0; +#else +constexpr int kDeletingDtorSlot = 1; +#endif + +VTableOverlay* +MakeVTableOverlay(void* inst, ConstDeclRef base, const ConstFuncRef* methods, + void* const* overlay_fns, std::size_t n_overlays, + std::size_t n_extra_prefix_slots, + VTableOverlayDtorHook on_destroy, void* cleanup_data) { + INTEROP_TRACE(inst, base, methods, overlay_fns, n_overlays, + n_extra_prefix_slots, on_destroy, cleanup_data); + // Refuse layouts the single-primary-vptr overlay cannot fully express, + // so the caller cannot silently produce mis-dispatching objects. Must run + // before vtableMethodSlotCount: on MSVC, getVFTableLayout(RD, offset 0) + // asserts when a virtual-inheritance class has no VFTable at that offset. + if (!inst) + return INTEROP_RETURN(nullptr); + const auto* RD = llvm::dyn_cast_or_null(unwrap(base)); + if (RD) + RD = RD->getDefinition(); + if (!RD || !RD->isPolymorphic() || hasComplexVTableLayout(RD)) + return INTEROP_RETURN(nullptr); + + int total_method_slots = vtableMethodSlotCount(base); + if (total_method_slots < kMinVTableMethodSlots) + return INTEROP_RETURN(nullptr); + + llvm::SmallVector slots; + slots.reserve(n_overlays); + for (std::size_t i = 0; i < n_overlays; ++i) { + int slot = virtualMethodSlot(methods[i]); + if (slot < 0) + return INTEROP_RETURN(nullptr); + slots.push_back(slot); + } + auto* ov = applyVTableOverlay(inst, total_method_slots, slots.data(), + overlay_fns, n_overlays, + n_extra_prefix_slots); + if (!ov) + return INTEROP_RETURN(nullptr); + + // Optional dtor hook: capture the original D0 (already copied into + // the block), wire the hook fields on the overlay, then publish the + // wrapper at the deleting-dtor slot. Ordering matters -- the fields + // must be set before the wrapper is reachable, otherwise a concurrent + // destruction could fire the wrapper with stale state. + if (on_destroy) { + void** vptr = ov->address_point(); + ov->orig_dtor = + VTableOverlay::BitCastFn(vptr[kDeletingDtorSlot]); + ov->cleanup = on_destroy; + ov->cleanup_data = cleanup_data; + vptr[kDeletingDtorSlot] = + VTableOverlay::BitCastFn(&cppinteropVTableOverlayDtorWrapper); + } + + return INTEROP_RETURN(ov); +} + +void DestroyVTableOverlay(VTableOverlay* overlay) { + INTEROP_TRACE(overlay); + delete overlay; // ~VTableOverlay restores vptr if dtor hasn't fired. + return INTEROP_VOID_RETURN(); +} + +void GetDatamembers(DeclRef DRef, std::vector& datamembers) { + INTEROP_TRACE(DRef, INTEROP_OUT(datamembers)); + auto* D = unwrap(DRef); if (auto* CXXRD = llvm::dyn_cast_or_null(D)) { getSema().ForceDeclarationOfImplicitMembers(CXXRD); @@ -2024,7 +2371,7 @@ void GetDatamembers(TCppScope_t scope, std::vector& datamembers) { } } } - datamembers.push_back((TCppScope_t)D); + datamembers.push_back(D); } else if (auto* USD = llvm::dyn_cast(D)) { if (llvm::isa(USD->getTargetDecl())) @@ -2036,21 +2383,21 @@ void GetDatamembers(TCppScope_t scope, std::vector& datamembers) { return INTEROP_VOID_RETURN(); } -void GetStaticDatamembers(TCppScope_t scope, - std::vector& datamembers) { - INTEROP_TRACE(scope, INTEROP_OUT(datamembers)); - GetClassDecls(scope, datamembers); +void GetStaticDatamembers(ConstDeclRef DRef, + std::vector& datamembers) { + INTEROP_TRACE(DRef, INTEROP_OUT(datamembers)); + GetClassDecls(DRef, datamembers); return INTEROP_VOID_RETURN(); } -void GetEnumConstantDatamembers(TCppScope_t scope, - std::vector& datamembers, +void GetEnumConstantDatamembers(ConstDeclRef DRef, + std::vector& datamembers, bool include_enum_class) { - INTEROP_TRACE(scope, INTEROP_OUT(datamembers), include_enum_class); - std::vector EDs; - GetClassDecls(scope, EDs); - for (TCppScope_t i : EDs) { - auto* ED = static_cast(i); + INTEROP_TRACE(DRef, INTEROP_OUT(datamembers), include_enum_class); + std::vector EDs; + GetClassDecls(DRef, EDs); + for (DeclRef i : EDs) { + auto* ED = unwrap(i); bool is_class_tagged = ED->isScopedUsingClassTag(); if (is_class_tagged && !include_enum_class) @@ -2062,51 +2409,51 @@ void GetEnumConstantDatamembers(TCppScope_t scope, return INTEROP_VOID_RETURN(); } -TCppScope_t LookupDatamember(const std::string& name, TCppScope_t parent) { +DeclRef LookupDatamember(const std::string& name, ConstDeclRef parent) { INTEROP_TRACE(name, parent); - clang::DeclContext* Within = 0; + const clang::DeclContext* Within = nullptr; if (parent) { - auto* D = (clang::Decl*)parent; + const auto* D = unwrap(parent); Within = llvm::dyn_cast(D); } auto* ND = CppInternal::utils::Lookup::Named(&getSema(), name, Within); if (ND && ND != (clang::NamedDecl*)-1) { if (llvm::isa_and_nonnull(ND)) { - return INTEROP_RETURN((TCppScope_t)ND); + return INTEROP_RETURN(ND); } } return INTEROP_RETURN(nullptr); } -bool IsLambdaClass(TCppType_t type) { - INTEROP_TRACE(type); - QualType QT = QualType::getFromOpaquePtr(type); +bool IsLambdaClass(ConstTypeRef TyRef) { + INTEROP_TRACE(TyRef); + QualType QT = QualType::getFromOpaquePtr(TyRef.data); if (auto* CXXRD = QT->getAsCXXRecordDecl()) { return INTEROP_RETURN(CXXRD->isLambda()); } return INTEROP_RETURN(false); } -TCppType_t GetVariableType(TCppScope_t var) { +TypeRef GetVariableType(ConstDeclRef var) { INTEROP_TRACE(var); - auto* D = static_cast(var); + const auto* D = unwrap(var); - if (auto DD = llvm::dyn_cast_or_null(D)) { + if (const auto* DD = llvm::dyn_cast_or_null(D)) { QualType QT = DD->getType(); - // Check if the type is a typedef type + // Check if the TyRef is a typedef TyRef if (QT->isTypedefNameType()) { return INTEROP_RETURN(QT.getAsOpaquePtr()); } - // Else, return the canonical type + // Else, return the canonical TyRef QT = QT.getCanonicalType(); return INTEROP_RETURN(QT.getAsOpaquePtr()); } - if (auto* ECD = llvm::dyn_cast_or_null(D)) + if (const auto* ECD = llvm::dyn_cast_or_null(D)) return INTEROP_RETURN(ECD->getType().getAsOpaquePtr()); return INTEROP_RETURN(nullptr); @@ -2153,7 +2500,7 @@ intptr_t GetVariableOffset(compat::Interpreter& I, Decl* D, size_t num_bases = GetNumBases(RD); bool flag = false; for (size_t i = 0; i < num_bases; i++) { - auto* CRD = static_cast(GetBaseClass(RD, i)); + auto* CRD = unwrap(GetBaseClass(RD, i)); direction[CRD] = RD; if (CRD == FieldParentRecordDecl) { flag = true; @@ -2221,38 +2568,40 @@ intptr_t GetVariableOffset(compat::Interpreter& I, Decl* D, return 0; } -intptr_t GetVariableOffset(TCppScope_t var, TCppScope_t parent) { +intptr_t GetVariableOffset(ConstDeclRef var, ConstDeclRef parent) { INTEROP_TRACE(var, parent); - auto* D = static_cast(var); - auto* RD = llvm::dyn_cast_or_null(static_cast(parent)); + // The internal overload may trigger JIT materialization — logically const. + auto* D = const_cast(unwrap(var)); + auto* RD = const_cast( + llvm::dyn_cast_or_null(unwrap(parent))); return INTEROP_RETURN(GetVariableOffset(getInterp(), D, RD)); } // Check if the Access Specifier of the variable matches the provided value. -bool CheckVariableAccess(TCppScope_t var, AccessSpecifier AS) { - auto* D = (Decl*)var; +bool CheckVariableAccess(ConstDeclRef var, AccessSpecifier AS) { + const auto* D = unwrap(var); return D->getAccess() == AS; } -bool IsPublicVariable(TCppScope_t var) { +bool IsPublicVariable(ConstDeclRef var) { INTEROP_TRACE(var); return INTEROP_RETURN(CheckVariableAccess(var, AccessSpecifier::AS_public)); } -bool IsProtectedVariable(TCppScope_t var) { +bool IsProtectedVariable(ConstDeclRef var) { INTEROP_TRACE(var); return INTEROP_RETURN( CheckVariableAccess(var, AccessSpecifier::AS_protected)); } -bool IsPrivateVariable(TCppScope_t var) { +bool IsPrivateVariable(ConstDeclRef var) { INTEROP_TRACE(var); return INTEROP_RETURN(CheckVariableAccess(var, AccessSpecifier::AS_private)); } -bool IsStaticVariable(TCppScope_t var) { +bool IsStaticVariable(ConstDeclRef var) { INTEROP_TRACE(var); - auto* D = (Decl*)var; + const auto* D = unwrap(var); if (llvm::isa_and_nonnull(D)) { return INTEROP_RETURN(true); } @@ -2260,26 +2609,26 @@ bool IsStaticVariable(TCppScope_t var) { return INTEROP_RETURN(false); } -bool IsConstVariable(TCppScope_t var) { +bool IsConstVariable(ConstDeclRef var) { INTEROP_TRACE(var); - auto* D = (clang::Decl*)var; + const auto* D = unwrap(var); - if (auto* VD = llvm::dyn_cast_or_null(D)) { + if (const auto* VD = llvm::dyn_cast_or_null(D)) { return INTEROP_RETURN(VD->getType().isConstQualified()); } return INTEROP_RETURN(false); } -bool IsRecordType(TCppType_t type) { - INTEROP_TRACE(type); - QualType QT = QualType::getFromOpaquePtr(type); +bool IsRecordType(ConstTypeRef TyRef) { + INTEROP_TRACE(TyRef); + QualType QT = QualType::getFromOpaquePtr(TyRef.data); return INTEROP_RETURN(QT->isRecordType()); } -bool IsPODType(TCppType_t type) { - INTEROP_TRACE(type); - QualType QT = QualType::getFromOpaquePtr(type); +bool IsPODType(ConstTypeRef TyRef) { + INTEROP_TRACE(TyRef); + QualType QT = QualType::getFromOpaquePtr(TyRef.data); if (QT.isNull()) return INTEROP_RETURN(false); @@ -2287,11 +2636,11 @@ bool IsPODType(TCppType_t type) { return INTEROP_RETURN(QT.isPODType(getASTContext())); } -bool IsIntegerType(TCppType_t type, Signedness* s) { - INTEROP_TRACE(type, s); - if (!type) +bool IsIntegerType(ConstTypeRef TyRef, Signedness* s) { + INTEROP_TRACE(TyRef, s); + if (!TyRef) return INTEROP_RETURN(false); - QualType QT = QualType::getFromOpaquePtr(type); + QualType QT = QualType::getFromOpaquePtr(TyRef.data); if (!QT->hasIntegerRepresentation()) return INTEROP_RETURN(false); if (s) { @@ -2301,54 +2650,54 @@ bool IsIntegerType(TCppType_t type, Signedness* s) { return INTEROP_RETURN(true); } -bool IsFloatingType(TCppType_t type) { - INTEROP_TRACE(type); - if (!type) +bool IsFloatingType(ConstTypeRef TyRef) { + INTEROP_TRACE(TyRef); + if (!TyRef) return INTEROP_RETURN(false); - QualType QT = QualType::getFromOpaquePtr(type); + QualType QT = QualType::getFromOpaquePtr(TyRef.data); return INTEROP_RETURN(QT->hasFloatingRepresentation()); } -bool IsSameType(TCppType_t type_a, TCppType_t type_b) { +bool IsSameType(ConstTypeRef type_a, ConstTypeRef type_b) { INTEROP_TRACE(type_a, type_b); if (!type_a || !type_b) return INTEROP_RETURN(false); - QualType QT1 = QualType::getFromOpaquePtr(type_a); - QualType QT2 = QualType::getFromOpaquePtr(type_b); + QualType QT1 = QualType::getFromOpaquePtr(type_a.data); + QualType QT2 = QualType::getFromOpaquePtr(type_b.data); return INTEROP_RETURN(getASTContext().hasSameType(QT1, QT2)); } -bool IsPointerType(TCppType_t type) { - INTEROP_TRACE(type); - QualType QT = QualType::getFromOpaquePtr(type); +bool IsPointerType(ConstTypeRef TyRef) { + INTEROP_TRACE(TyRef); + QualType QT = QualType::getFromOpaquePtr(TyRef.data); return INTEROP_RETURN(QT->isPointerType()); } -bool IsVoidPointerType(TCppType_t type) { - INTEROP_TRACE(type); - if (!type) +bool IsVoidPointerType(ConstTypeRef TyRef) { + INTEROP_TRACE(TyRef); + if (!TyRef) return INTEROP_RETURN(false); - QualType QT = QualType::getFromOpaquePtr(type); + QualType QT = QualType::getFromOpaquePtr(TyRef.data); return INTEROP_RETURN(QT->isVoidPointerType()); } -TCppType_t GetPointeeType(TCppType_t type) { - INTEROP_TRACE(type); - if (!IsPointerType(type)) +TypeRef GetPointeeType(ConstTypeRef TyRef) { + INTEROP_TRACE(TyRef); + if (!IsPointerType(TyRef)) return INTEROP_RETURN(nullptr); - QualType QT = QualType::getFromOpaquePtr(type); + QualType QT = QualType::getFromOpaquePtr(TyRef.data); return INTEROP_RETURN(QT->getPointeeType().getAsOpaquePtr()); } -bool IsReferenceType(TCppType_t type) { - INTEROP_TRACE(type); - QualType QT = QualType::getFromOpaquePtr(type); +bool IsReferenceType(ConstTypeRef TyRef) { + INTEROP_TRACE(TyRef); + QualType QT = QualType::getFromOpaquePtr(TyRef.data); return INTEROP_RETURN(QT->isReferenceType()); } -ValueKind GetValueKind(TCppType_t type) { - INTEROP_TRACE(type); - QualType QT = QualType::getFromOpaquePtr(type); +ValueKind GetValueKind(ConstTypeRef TyRef) { + INTEROP_TRACE(TyRef); + QualType QT = QualType::getFromOpaquePtr(TyRef.data); if (QT->isRValueReferenceType()) return INTEROP_RETURN(ValueKind::RValue); if (QT->isLValueReferenceType()) @@ -2356,15 +2705,15 @@ ValueKind GetValueKind(TCppType_t type) { return INTEROP_RETURN(ValueKind::None); } -TCppType_t GetPointerType(TCppType_t type) { - INTEROP_TRACE(type); - QualType QT = QualType::getFromOpaquePtr(type); +TypeRef GetPointerType(ConstTypeRef TyRef) { + INTEROP_TRACE(TyRef); + QualType QT = QualType::getFromOpaquePtr(TyRef.data); return INTEROP_RETURN(getASTContext().getPointerType(QT).getAsOpaquePtr()); } -TCppType_t GetReferencedType(TCppType_t type, bool rvalue) { - INTEROP_TRACE(type, rvalue); - QualType QT = QualType::getFromOpaquePtr(type); +TypeRef GetReferencedType(ConstTypeRef TyRef, bool rvalue) { + INTEROP_TRACE(TyRef, rvalue); + QualType QT = QualType::getFromOpaquePtr(TyRef.data); if (rvalue) return INTEROP_RETURN( getASTContext().getRValueReferenceType(QT).getAsOpaquePtr()); @@ -2372,19 +2721,19 @@ TCppType_t GetReferencedType(TCppType_t type, bool rvalue) { getASTContext().getLValueReferenceType(QT).getAsOpaquePtr()); } -TCppType_t GetNonReferenceType(TCppType_t type) { - INTEROP_TRACE(type); - if (!IsReferenceType(type)) +TypeRef GetNonReferenceType(ConstTypeRef TyRef) { + INTEROP_TRACE(TyRef); + if (!IsReferenceType(TyRef)) return INTEROP_RETURN(nullptr); - QualType QT = QualType::getFromOpaquePtr(type); + QualType QT = QualType::getFromOpaquePtr(TyRef.data); return INTEROP_RETURN(QT.getNonReferenceType().getAsOpaquePtr()); } -TCppType_t GetUnderlyingType(TCppType_t type) { - INTEROP_TRACE(type); - if (!type) +TypeRef GetUnderlyingType(ConstTypeRef TyRef) { + INTEROP_TRACE(TyRef); + if (!TyRef) return INTEROP_RETURN(nullptr); - QualType QT = QualType::getFromOpaquePtr(type); + QualType QT = QualType::getFromOpaquePtr(TyRef.data); QT = QT->getCanonicalTypeUnqualified(); // Recursively remove array dimensions @@ -2392,7 +2741,7 @@ TCppType_t GetUnderlyingType(TCppType_t type) { QT = QualType(QT->getArrayElementTypeNoTypeQual(), 0); // Recursively reduce pointer depth till we are left with a pointerless - // type. + // TyRef. for (auto PT = QT->getPointeeType(); !PT.isNull(); PT = QT->getPointeeType()) { QT = PT; @@ -2401,9 +2750,9 @@ TCppType_t GetUnderlyingType(TCppType_t type) { return INTEROP_RETURN(QT.getAsOpaquePtr()); } -std::string GetTypeAsString(TCppType_t var) { +std::string GetTypeAsString(ConstTypeRef var) { INTEROP_TRACE(var); - QualType QT = QualType::getFromOpaquePtr(var); + QualType QT = QualType::getFromOpaquePtr(var.data); PrintingPolicy Policy(getASTContext().getPrintingPolicy()); Policy.Bool = true; // Print bool instead of _Bool. Policy.SuppressTagKeyword = true; // Do not print `class std::string`. @@ -2412,20 +2761,20 @@ std::string GetTypeAsString(TCppType_t var) { return INTEROP_RETURN(QT.getAsString(Policy)); } -TCppType_t GetCanonicalType(TCppType_t type) { - INTEROP_TRACE(type); - if (!type) +TypeRef GetCanonicalType(ConstTypeRef TyRef) { + INTEROP_TRACE(TyRef); + if (!TyRef) return INTEROP_RETURN(nullptr); - QualType QT = QualType::getFromOpaquePtr(type); + QualType QT = QualType::getFromOpaquePtr(TyRef.data); return INTEROP_RETURN(QT.getCanonicalType().getAsOpaquePtr()); } -bool HasTypeQualifier(TCppType_t type, QualKind qual) { - INTEROP_TRACE(type, qual); - if (!type) +bool HasTypeQualifier(ConstTypeRef TyRef, QualKind qual) { + INTEROP_TRACE(TyRef, qual); + if (!TyRef) return INTEROP_RETURN(false); - QualType QT = QualType::getFromOpaquePtr(type); + QualType QT = QualType::getFromOpaquePtr(TyRef.data); if (qual & QualKind::Const) { if (!QT.isConstQualified()) return INTEROP_RETURN(false); @@ -2441,12 +2790,12 @@ bool HasTypeQualifier(TCppType_t type, QualKind qual) { return INTEROP_RETURN(true); } -TCppType_t RemoveTypeQualifier(TCppType_t type, QualKind qual) { - INTEROP_TRACE(type, qual); - if (!type) - return INTEROP_RETURN(type); +TypeRef RemoveTypeQualifier(ConstTypeRef TyRef, QualKind qual) { + INTEROP_TRACE(TyRef, qual); + if (!TyRef) + return INTEROP_RETURN(nullptr); - auto QT = QualType(QualType::getFromOpaquePtr(type)); + auto QT = QualType(QualType::getFromOpaquePtr(TyRef.data)); if (qual & QualKind::Const) QT.removeLocalConst(); if (qual & QualKind::Volatile) @@ -2456,12 +2805,12 @@ TCppType_t RemoveTypeQualifier(TCppType_t type, QualKind qual) { return INTEROP_RETURN(QT.getAsOpaquePtr()); } -TCppType_t AddTypeQualifier(TCppType_t type, QualKind qual) { - INTEROP_TRACE(type, qual); - if (!type) - return INTEROP_RETURN(type); +TypeRef AddTypeQualifier(ConstTypeRef TyRef, QualKind qual) { + INTEROP_TRACE(TyRef, qual); + if (!TyRef) + return INTEROP_RETURN(nullptr); - auto QT = QualType(QualType::getFromOpaquePtr(type)); + auto QT = QualType(QualType::getFromOpaquePtr(TyRef.data)); if (qual & QualKind::Const) { if (!QT.isConstQualified()) QT.addConst(); @@ -2581,7 +2930,7 @@ static QualType findBuiltinType(llvm::StringRef typeName, ASTContext& Context) { return QualType(); // Return null if not a builtin } -static std::optional GetTypeInternal(Decl* D) { +static std::optional GetTypeInternal(const Decl* D) { if (!D) return {}; // Even though typedefs derive from TypeDecl, their getTypeForDecl() @@ -2589,20 +2938,16 @@ static std::optional GetTypeInternal(Decl* D) { if (const auto* TND = llvm::dyn_cast_or_null(D)) return TND->getUnderlyingType(); - if (auto* VD = dyn_cast(D)) + if (const auto* VD = dyn_cast(D)) return VD->getType(); if (const auto* TD = llvm::dyn_cast_or_null(D)) -#if CLANG_VERSION_MAJOR < 22 - return QualType(TD->getTypeForDecl(), 0); -#else - return getASTContext().getTypeDeclType(TD); -#endif + return compat::GetTypeFromDecl(TD); return {}; } -TCppType_t GetType(const std::string& name, TCppScope_t parent /*= nullptr*/) { +TypeRef GetType(const std::string& name, ConstDeclRef parent /*= nullptr*/) { INTEROP_TRACE(name, parent); QualType builtin = findBuiltinType(name, getASTContext()); if (!builtin.isNull()) @@ -2611,19 +2956,19 @@ TCppType_t GetType(const std::string& name, TCppScope_t parent /*= nullptr*/) { return INTEROP_RETURN(GetTypeFromScope(GetNamed(name, parent))); } -TCppType_t GetComplexType(TCppType_t type) { - INTEROP_TRACE(type); - QualType QT = QualType::getFromOpaquePtr(type); +TypeRef GetComplexType(ConstTypeRef TyRef) { + INTEROP_TRACE(TyRef); + QualType QT = QualType::getFromOpaquePtr(TyRef.data); return INTEROP_RETURN(getASTContext().getComplexType(QT).getAsOpaquePtr()); } -TCppType_t GetTypeFromScope(TCppScope_t klass) { - INTEROP_TRACE(klass); - if (!klass) +TypeRef GetTypeFromScope(ConstDeclRef DRef) { + INTEROP_TRACE(DRef); + if (!DRef) return INTEROP_RETURN(nullptr); - if (auto QT = GetTypeInternal((Decl*)klass)) + if (auto QT = GetTypeInternal(unwrap(DRef))) return INTEROP_RETURN(QT->getAsOpaquePtr()); return INTEROP_RETURN(nullptr); @@ -2660,6 +3005,18 @@ void get_type_as_string(QualType QT, std::string& type_name, ASTContext& C, PrintingPolicy Policy) { // TODO: Implement cling desugaring from utils::AST // cling::utils::Transform::GetPartiallyDesugaredType() + // Desugar template type alias specializations (e.g. std::enable_if_t, + // std::remove_cvref_t). Their printed form can carry expression-level + // template arguments (variable-template references, SFINAE predicates) + // that PrintingPolicy::FullyQualifiedName does not propagate into, so the + // emitted text may reference identifiers like `is_constructible_v` without + // the `std::` qualifier and fail to compile in the wrapper. Regular + // typedefs (e.g. std::string) keep their sugared name. + while (const auto* TST = QT->getAs()) { + if (!TST->isTypeAlias()) + break; + QT = TST->desugar(); + } if (!QT->isTypedefNameType() || QT->isBuiltinType()) QT = QT.getDesugaredType(C); Policy.Suppress_Elab = true; @@ -2676,20 +3033,16 @@ static void GetDeclName(const clang::Decl* D, ASTContext& Context, Policy.SuppressTagKeyword = true; Policy.SuppressUnwrittenScope = true; Policy.Print_Canonical_Types = true; - if (const TypeDecl* TD = dyn_cast(D)) { + if (const auto* TD = dyn_cast(D)) { // This is a class, struct, or union member. QualType QT; - if (const TypedefDecl* Typedef = dyn_cast(TD)) { + if (const auto* Typedef = dyn_cast(TD)) { // Handle the typedefs to anonymous types. QT = Typedef->getTypeSourceInfo()->getType(); } else -#if CLANG_VERSION_MAJOR < 22 - QT = {TD->getTypeForDecl(), 0}; -#else - QT = TD->getASTContext().getTypeDeclType(TD); -#endif + QT = compat::GetTypeFromDecl(TD); get_type_as_string(QT, name, Context, Policy); - } else if (const NamedDecl* ND = dyn_cast(D)) { + } else if (const auto* ND = dyn_cast(D)) { // This is a namespace member. raw_string_ostream stream(name); ND->getNameForDiagnostic(stream, Policy, /*Qualified=*/true); @@ -2703,7 +3056,7 @@ void collect_type_info(const FunctionDecl* FD, QualType& QT, EReferenceType& refType, bool& isPointer, int indent_level, bool forArgument) { // - // Collect information about the type of a function parameter + // Collect information about the TyRef of a function parameter // needed for building the wrapper function. // ASTContext& C = FD->getASTContext(); @@ -2729,20 +3082,27 @@ void collect_type_info(const FunctionDecl* FD, QualType& QT, } } } - if (QT->isFunctionPointerType()) { + if (QT.getNonReferenceType()->isFunctionPointerType() || + QT.getNonReferenceType()->isFunctionProtoType()) { + clang::QualType NRQT = QT.getNonReferenceType(); std::string fp_typedef_name; { std::ostringstream nm; nm << "FP" << gWrapperSerial++; type_name = nm.str(); raw_string_ostream OS(fp_typedef_name); - QT.print(OS, Policy, type_name); + NRQT.print(OS, Policy, type_name); OS.flush(); } indent(typedefbuf, indent_level); typedefbuf << "typedef " << fp_typedef_name << ";\n"; + + if (QT->isRValueReferenceType()) + refType = kRValueReference; + else + refType = kLValueReference; return; } else if (QT->isMemberPointerType()) { std::string mp_typedef_name; @@ -2769,8 +3129,8 @@ void collect_type_info(const FunctionDecl* FD, QualType& QT, refType = kLValueReference; QT = cast(QT.getCanonicalType())->getPointeeType(); } - // Fall through for the array type to deal with reference/pointer ro array - // type. + // Fall through for the array TyRef to deal with reference/pointer ro array + // TyRef. if (QT->isArrayType()) { std::string ar_typedef_name; { @@ -2845,7 +3205,7 @@ void make_narg_ctor(const FunctionDecl* FD, const unsigned N, } const DeclContext* get_non_transparent_decl_context(const FunctionDecl* FD) { - auto* DC = FD->getDeclContext(); + const auto* DC = FD->getDeclContext(); while (DC->isTransparentContext()) { DC = DC->getParent(); assert(DC && "All transparent contexts should have a parent!"); @@ -2860,11 +3220,11 @@ void make_narg_call(const FunctionDecl* FD, const std::string& return_type, // // Make a code string that follows this pattern: // - // ((*)obj)->(*(*)args[i], ...) + // ((*)obj)->(*(*)args[i], ...) // // Sometimes it's necessary that we cast the function we want to call - // first to its explicit function type before calling it. This is supposed + // first to its explicit function TyRef before calling it. This is supposed // to prevent that we accidentally ending up in a function that is not // the one we're supposed to call here (e.g. because the C++ function // lookup decides to take another function that better fits). This method @@ -2898,7 +3258,7 @@ void make_narg_call(const FunctionDecl* FD, const std::string& return_type, indent(callbuf, indent_level + 1); } } - const ParmVarDecl* PVD = FD->getParamDecl(i); + const ParmVarDecl* PVD = FD->getNonObjectParameter(i); QualType Ty = PVD->getType(); QualType QT = Ty.getCanonicalType(); std::string arg_type; @@ -2914,9 +3274,18 @@ void make_narg_call(const FunctionDecl* FD, const std::string& return_type, callbuf << ")"; } - if (const CXXMethodDecl* MD = dyn_cast(FD)) { + if (const auto* MD = dyn_cast(FD)) { // This is a class, struct, or union member. - if (MD->isConst()) + // An rvalue-ref-qualified method must be called on an rvalue: bind the + // receiver with static_cast. Covers `f() &&` and `this T&&` (the + // latter leaves getRefQualifier() == RQ_None). + bool rvalue_ref = MD->getRefQualifier() == clang::RQ_RValue || + (MD->hasCXXExplicitFunctionObjectParameter() && + MD->getParamDecl(0)->getType()->isRValueReferenceType()); + if (rvalue_ref) + callbuf << "static_cast<" << class_name << "&&>(*(" << class_name + << "*)obj)."; + else if (MD->isConst()) callbuf << "((const " << class_name << "*)obj)->"; else callbuf << "((" << class_name << "*)obj)->"; @@ -2943,13 +3312,13 @@ void make_narg_call(const FunctionDecl* FD, const std::string& return_type, name = complete_name; // If a template has consecutive parameter packs, then it is impossible to - // use the explicit name in the wrapper, since the type deduction is what + // use the explicit name in the wrapper, since the TyRef deduction is what // determines the split of the packs. Instead, we'll revert to the - // non-templated function name and hope that the type casts in the wrapper - // will suffice. + // non-templated function name and hope that the TyRef casts in the + // wrapper will suffice. std::string simple_name = FD->getNameAsString(); if (FD->isTemplateInstantiation() && FD->getPrimaryTemplate()) { - const FunctionTemplateDecl* FTDecl = + const auto* FTDecl = llvm::dyn_cast(FD->getPrimaryTemplate()); if (FTDecl) { auto* templateParms = FTDecl->getTemplateParameters(); @@ -2977,7 +3346,7 @@ void make_narg_call(const FunctionDecl* FD, const std::string& return_type, callbuf << "("; for (unsigned i = 0U; i < N; ++i) { - const ParmVarDecl* PVD = FD->getParamDecl(i); + const ParmVarDecl* PVD = FD->getNonObjectParameter(i); QualType Ty = PVD->getType(); QualType QT = Ty.getCanonicalType(); std::string type_name; @@ -3133,7 +3502,7 @@ void make_narg_call_with_return(compat::Interpreter& I, const FunctionDecl* FD, // (void)(((class_name*)obj)->func(args...)); // } // - if (const CXXConstructorDecl* CD = dyn_cast(FD)) { + if (const auto* CD = dyn_cast(FD)) { if (N <= 1 && llvm::isa(FD)) { auto SpecMemKind = I.getCI()->getSema().getSpecialMember(CD); if ((N == 0 && SpecMemKind == CXXSpecialMemberKind::DefaultConstructor) || @@ -3184,7 +3553,7 @@ void make_narg_call_with_return(compat::Interpreter& I, const FunctionDecl* FD, indent(callbuf, indent_level); callbuf << "new (ret) "; // - // Write the type part of the placement new. + // Write the TyRef part of the placement new. // callbuf << "(" << type_name.c_str(); if (refType != kNotReference) { @@ -3335,7 +3704,7 @@ int get_wrapper_code(compat::Interpreter& I, const FunctionDecl* FD, case FunctionDecl::TK_FunctionTemplateSpecialization: { // This function is the result of instantiating a function // template or possibly an explicit specialization of a - // function template. Could be a namespace scope function or a + // function template. Could be a namespace DRef function or a // member function. if (!FD->isTemplateInstantiation()) { // We are either TSK_Undeclared or @@ -3540,7 +3909,7 @@ int get_wrapper_code(compat::Interpreter& I, const FunctionDecl* FD, case FunctionDecl::TK_FunctionTemplateSpecialization: { // This function is the result of instantiating a function // template or possibly an explicit specialization of a - // function template. Could be a namespace scope function or a + // function template. Could be a namespace DRef function or a // member function. if (Definition->isDeleted()) { llvm::errs() << "TClingCallFunc::make_wrapper" @@ -3600,15 +3969,18 @@ int get_wrapper_code(compat::Interpreter& I, const FunctionDecl* FD, } break; } } - unsigned min_args = FD->getMinRequiredArguments(); - unsigned num_params = FD->getNumParams(); + // A C++23 explicit object parameter is bound via the `obj->` receiver of the + // emitted member call, not from the args[] array, so it is excluded from the + // wrapper's argument arity (see make_narg_call). + unsigned min_args = FD->getMinRequiredExplicitArguments(); + unsigned num_params = FD->getNumNonObjectParams(); // // Make the wrapper name. // { std::ostringstream buf; buf << "__jc"; - // const NamedDecl* ND = dyn_cast(FD); + // const auto* ND = dyn_cast(FD); // std::string mn; // fInterp->maybeMangleDeclName(ND, mn); // buf << '_' << mn; @@ -3633,7 +4005,7 @@ int get_wrapper_code(compat::Interpreter& I, const FunctionDecl* FD, "__attribute__((annotate(\"__cling__ptrcheck(off)\")))\n" "extern \"C\" void "; buf << wrapper_name; - if (Cpp::IsConstructor(FD)) { + if (Cpp::IsConstructor(wrap(FD))) { buf << "(void* ret, unsigned long nary, unsigned long nargs, void** args, " "void* is_arena)\n" "{\n"; @@ -3737,7 +4109,7 @@ static std::string PrepareStructorWrapper(const Decl* D, { std::ostringstream buf; buf << wrapper_prefix; - // const NamedDecl* ND = dyn_cast(FD); + // const auto* ND = dyn_cast(FD); // string mn; // fInterp->maybeMangleDeclName(ND, mn); // buf << '_dtor_' << mn; @@ -3914,39 +4286,40 @@ static JitCall::DestructorCall make_dtor_wrapper(compat::Interpreter& interp, } // namespace // End of JitCall Helper Functions -CPPINTEROP_API JitCall MakeFunctionCallable(TInterp_t I, - TCppConstFunction_t func) { +CPPINTEROP_API JitCall MakeFunctionCallable(InterpRef I, ConstFuncRef func) { INTEROP_TRACE(I, func); - const auto* D = static_cast(func); + const auto* D = unwrap(func); if (!D) return INTEROP_RETURN(JitCall{}); - auto* interp = static_cast(I); + auto* interp = unwrap(I); // FIXME: Unify with make_wrapper. if (const auto* Dtor = dyn_cast(D)) { if (auto Wrapper = make_dtor_wrapper(*interp, Dtor->getParent())) - return INTEROP_RETURN(JitCall(JitCall::kDestructorCall, Wrapper, Dtor)); + return INTEROP_RETURN( + JitCall(JitCall::kDestructorCall, Wrapper, wrap(Dtor))); // FIXME: else error we failed to compile the wrapper. return INTEROP_RETURN(JitCall{}); } if (const auto* Ctor = dyn_cast(D)) { if (auto Wrapper = make_wrapper(*interp, cast(D))) - return INTEROP_RETURN(JitCall(JitCall::kConstructorCall, Wrapper, Ctor)); + return INTEROP_RETURN(JitCall(JitCall::kConstructorCall, Wrapper, + wrap(Ctor))); // FIXME: else error we failed to compile the wrapper. return INTEROP_RETURN(JitCall{}); } if (auto Wrapper = make_wrapper(*interp, cast(D))) { - return INTEROP_RETURN( - JitCall(JitCall::kGenericCall, Wrapper, cast(D))); + return INTEROP_RETURN(JitCall(JitCall::kGenericCall, Wrapper, + wrap(cast(D)))); } // FIXME: else error we failed to compile the wrapper. return INTEROP_RETURN(JitCall{}); } -CPPINTEROP_API JitCall MakeFunctionCallable(TCppConstFunction_t func) { +CPPINTEROP_API JitCall MakeFunctionCallable(ConstFuncRef func) { INTEROP_TRACE(func); return INTEROP_RETURN(MakeFunctionCallable(&getInterp(), func)); } @@ -4035,7 +4408,36 @@ std::string ExtractArgument(const std::vector& Args, } } // namespace -TInterp_t CreateInterpreter(const std::vector& Args /*={}*/, +///\returns 0 on success. +static bool exec(const char* cmd, std::vector& outputs) { +#define DEBUG_TYPE "exec" + + std::array buffer; + struct file_deleter { + void operator()(FILE* fp) { pclose(fp); } + }; + std::unique_ptr pipe{popen(cmd, "r")}; + LLVM_DEBUG(dbgs() << "Executing command '" << cmd << "'\n"); + + if (!pipe) { + LLVM_DEBUG(dbgs() << "Execute failed!\n"); + perror("exec: "); + return false; + } + + LLVM_DEBUG(dbgs() << "Execute returned:\n"); + while (fgets(buffer.data(), static_cast(buffer.size()), pipe.get())) { + LLVM_DEBUG(dbgs() << buffer.data()); + llvm::StringRef trimmed = buffer.data(); + outputs.push_back(trimmed.trim().str()); + } + +#undef DEBUG_TYPE + + return true; +} + +InterpRef CreateInterpreter(const std::vector& Args /*={}*/, const std::vector& GpuArgs /*={}*/) { INTEROP_TRACE(Args, GpuArgs); std::string MainExecutableName = sys::fs::getMainExecutable(nullptr, nullptr); @@ -4053,8 +4455,9 @@ TInterp_t CreateInterpreter(const std::vector& Args /*={}*/, (T.isOSDarwin() || T.isOSLinux())) ResourceDir = DetectResourceDir(); - std::vector ClingArgv = {"-resource-dir", ResourceDir.c_str(), - "-std=c++14"}; + std::vector ClingArgv = {"-std=c++14"}; + if (!ResourceDir.empty()) + ClingArgv.insert(ClingArgv.begin(), {"-resource-dir", ResourceDir.c_str()}); ClingArgv.insert(ClingArgv.begin(), MainExecutableName.c_str()); #ifdef _WIN32 // FIXME : Workaround Sema::PushDeclContext assert on windows @@ -4117,6 +4520,33 @@ TInterp_t CreateInterpreter(const std::vector& Args /*={}*/, std::back_inserter(ClingArgv), [&](const std::string& str) { return str.c_str(); }); + // Figure out the right SDK path for MacOS. Mirrors the clang driver's + // resolution (Darwin::AddDeploymentTarget): try an explicit -isysroot, + // else a valid SDKROOT. Only when neither is usable fall back to + // `xcrun --show-sdk-path` (same query xcrun performs to set SDKROOT) + // This way a packaged (pip/conda) interpreter finds the active + // with no env config, relocatably across Xcode updates. + std::string MacOSSDK; + if (T.isOSDarwin()) { + const bool HasSysroot = llvm::any_of(ClingArgv, [](const char* A) { + return llvm::StringRef(A) == "-isysroot"; + }); + auto SDKRootEnv = llvm::sys::Process::GetEnv("SDKROOT"); + const bool ValidSDKRoot = SDKRootEnv && + llvm::sys::path::is_absolute(*SDKRootEnv) && + llvm::sys::fs::exists(*SDKRootEnv) && + llvm::StringRef(*SDKRootEnv) != "/"; + if (!HasSysroot && !ValidSDKRoot) { + std::vector Out; + if (exec("xcrun --sdk macosx --show-sdk-path", Out) && !Out.empty()) + MacOSSDK = Out.back(); + if (!MacOSSDK.empty() && llvm::sys::fs::is_directory(MacOSSDK)) { + ClingArgv.push_back("-isysroot"); + ClingArgv.push_back(MacOSSDK.c_str()); + } + } + } + // Force global process initialization. (void)GetInterpreters(); @@ -4175,8 +4605,8 @@ TInterp_t CreateInterpreter(const std::vector& Args /*={}*/, reinterpret_cast(&__clang_Interpreter_SetValueWithAlloc)); #else // obtain mangled name - auto* D = static_cast( - Cpp::GetNamed("__clang_Interpreter_SetValueWithAlloc")); + auto* D = + unwrap(Cpp::GetNamed("__clang_Interpreter_SetValueWithAlloc")); if (auto* FD = llvm::dyn_cast_or_null(D)) { auto GD = GlobalDecl(FD); std::string mangledName; @@ -4194,7 +4624,7 @@ TInterp_t CreateInterpreter(const std::vector& Args /*={}*/, return INTEROP_RETURN(I); } -InterpreterLanguage GetLanguage(TInterp_t I /*=nullptr*/) { +InterpreterLanguage GetLanguage(InterpRef I /*=nullptr*/) { INTEROP_TRACE(I); compat::Interpreter* interp = &getInterp(I); const auto& LO = interp->getCI()->getLangOpts(); @@ -4214,7 +4644,7 @@ InterpreterLanguage GetLanguage(TInterp_t I /*=nullptr*/) { return INTEROP_RETURN(lang); } -InterpreterLanguageStandard GetLanguageStandard(TInterp_t I /*=nullptr*/) { +InterpreterLanguageStandard GetLanguageStandard(InterpRef I /*=nullptr*/) { INTEROP_TRACE(I); compat::Interpreter* interp = &getInterp(I); const auto& LO = interp->getCI()->getLangOpts(); @@ -4240,35 +4670,6 @@ const char* GetResourceDir() { getInterp().getCI()->getHeaderSearchOpts().ResourceDir.c_str()); } -///\returns 0 on success. -static bool exec(const char* cmd, std::vector& outputs) { -#define DEBUG_TYPE "exec" - - std::array buffer; - struct file_deleter { - void operator()(FILE* fp) { pclose(fp); } - }; - std::unique_ptr pipe{popen(cmd, "r")}; - LLVM_DEBUG(dbgs() << "Executing command '" << cmd << "'\n"); - - if (!pipe) { - LLVM_DEBUG(dbgs() << "Execute failed!\n"); - perror("exec: "); - return false; - } - - LLVM_DEBUG(dbgs() << "Execute returned:\n"); - while (fgets(buffer.data(), static_cast(buffer.size()), pipe.get())) { - LLVM_DEBUG(dbgs() << buffer.data()); - llvm::StringRef trimmed = buffer.data(); - outputs.push_back(trimmed.trim().str()); - } - -#undef DEBUG_TYPE - - return true; -} - std::string DetectResourceDir(const char* ClangBinaryName /* = clang */) { INTEROP_TRACE(ClangBinaryName); std::string cmd = std::string(ClangBinaryName) + " -print-resource-dir"; @@ -4360,24 +4761,85 @@ int Process(const char* code) { return INTEROP_RETURN(getInterp().process(code)); } -intptr_t Evaluate(const char* code, bool* IsValueInvalid /*=nullptr*/) { - INTEROP_TRACE(code, INTEROP_OUT(IsValueInvalid)); - compat::Value V; - - if (IsValueInvalid) - *IsValueInvalid = false; +// Classify the QualType of a successfully-evaluated value into a +// Box::Kind. clang::Value's own ctor asserts on builtins the X-macro +// doesn't list (`__int128`, `_BitInt`, `_Float16`, ...), so by the time +// we get here QT is non-null and BT->getKind() is one of the enumerated +// arms. Records, pointers and references fall through to K_PtrOrObj. +// See memory/clang_value_wide_types_gap.md for the upstream follow-up +// that would broaden Value's coverage. +static Cpp::Box::Kind classifyByQualType(clang::QualType QT) { + if (const auto* BT = QT->getAs()) { + switch (BT->getKind()) { + case clang::BuiltinType::Bool: + return Cpp::Box::K_Bool; + case clang::BuiltinType::Char_S: + return Cpp::Box::K_Char_S; + case clang::BuiltinType::Char_U: + // Platform-`unsigned`-char alias; share UChar storage so the + // X-macro doesn't need a duplicate Box::Create specialization. + return Cpp::Box::K_UChar; + case clang::BuiltinType::SChar: + return Cpp::Box::K_SChar; + case clang::BuiltinType::UChar: + return Cpp::Box::K_UChar; + case clang::BuiltinType::Short: + return Cpp::Box::K_Short; + case clang::BuiltinType::UShort: + return Cpp::Box::K_UShort; + case clang::BuiltinType::Int: + return Cpp::Box::K_Int; + case clang::BuiltinType::UInt: + return Cpp::Box::K_UInt; + case clang::BuiltinType::Long: + return Cpp::Box::K_Long; + case clang::BuiltinType::ULong: + return Cpp::Box::K_ULong; + case clang::BuiltinType::LongLong: + return Cpp::Box::K_LongLong; + case clang::BuiltinType::ULongLong: + return Cpp::Box::K_ULongLong; + case clang::BuiltinType::Float: + return Cpp::Box::K_Float; + case clang::BuiltinType::Double: + return Cpp::Box::K_Double; + case clang::BuiltinType::LongDouble: + return Cpp::Box::K_LongDouble; + default: + llvm_unreachable( + "clang::Value asserts on builtins outside the X-macro set"); + } + } + return Cpp::Box::K_PtrOrObj; +} +Box Evaluate(const char* code) { + INTEROP_TRACE(code); + compat::Value V; auto res = getInterp().evaluate(code, V); CPPINTEROP_MSAN_UNPOISON_VALUE(V); - // 0 is success; an unset V on success means convertTo would assert. - if (res != 0 || !V.hasValue()) { - if (IsValueInvalid) - *IsValueInvalid = true; - // FIXME: Make this return llvm::Expected - return INTEROP_RETURN(~0UL); - } - - return INTEROP_RETURN(compat::convertTo(V)); + if (res != 0 || !V.hasValue()) + return INTEROP_RETURN(Box{}); + + clang::QualType QT = V.getType(); + void* qt = QT.getAsOpaquePtr(); + switch (classifyByQualType(QT)) { +#define X(TyRef, name) \ + case Cpp::Box::K_##name: \ + return INTEROP_RETURN( \ + Cpp::Box::Create(compat::convertTo(V), qt)); + CPP_BOX_BUILTIN_TYPES +#undef X + case Cpp::Box::K_PtrOrObj: + return INTEROP_RETURN(compat::MakeValueBox(V, qt)); + case Cpp::Box::K_Char_U: + case Cpp::Box::K_Void: + case Cpp::Box::K_Unspecified: + // classifyByQualType never produces these (Char_U folds to UChar; + // Void/Unspecified can't reach a hasValue=true path). + llvm_unreachable("Box::Kind not produced by classifyByQualType"); + } + llvm_unreachable("classifyByQualType returned an unhandled Kind"); } std::string LookupLibrary(const char* lib_name) { @@ -4494,9 +4956,9 @@ bool InsertOrReplaceJitSymbol(const char* linker_mangled_name, InsertOrReplaceJitSymbol(getInterp(), linker_mangled_name, address)); } -std::string ObjToString(const char* type, void* obj) { - INTEROP_TRACE(type, obj); - return INTEROP_RETURN(getInterp().toString(type, obj)); +std::string ObjToString(const char* TyRef, void* obj) { + INTEROP_TRACE(TyRef, obj); + return INTEROP_RETURN(getInterp().toString(TyRef, obj)); } static Decl* InstantiateTemplate(TemplateDecl* TemplateD, @@ -4533,7 +4995,7 @@ static Decl* InstantiateTemplate(TemplateDecl* TemplateD, return R.get(); } - // This will instantiate tape type and return it. + // This will instantiate tape TyRef and return it. SourceLocation noLoc; #if CLANG_VERSION_MAJOR < 22 QualType TT = S.CheckTemplateIdType(TemplateName(TemplateD), noLoc, TLI); @@ -4555,7 +5017,7 @@ static Decl* InstantiateTemplate(TemplateDecl* TemplateD, // CSS.Extend(C, GetCladNamespace(), noLoc, noLoc); // NestedNameSpecifier* NS = CSS.getScopeRep(); - // // Create elaborated type with namespace specifier, + // // Create elaborated TyRef with namespace specifier, // // i.e. class -> clad::class // return C.getElaboratedType(ETK_None, NS, TT); } @@ -4572,10 +5034,9 @@ Decl* InstantiateTemplate(TemplateDecl* TemplateD, return InstantiateTemplate(TemplateD, TLI, S, instantiate_body); } -TCppScope_t InstantiateTemplate(compat::Interpreter& I, TCppScope_t tmpl, - const TemplateArgInfo* template_args, - size_t template_args_size, - bool instantiate_body) { +DeclRef InstantiateTemplate(compat::Interpreter& I, DeclRef tmpl, + const TemplateArgInfo* template_args, + size_t template_args_size, bool instantiate_body) { auto& S = I.getSema(); auto& C = S.getASTContext(); @@ -4584,7 +5045,7 @@ TCppScope_t InstantiateTemplate(compat::Interpreter& I, TCppScope_t tmpl, for (size_t i = 0; i < template_args_size; ++i) { QualType ArgTy = QualType::getFromOpaquePtr(template_args[i].m_Type); if (template_args[i].m_IntegralValue) { - // We have a non-type template parameter. Create an integral value from + // We have a non-TyRef template parameter. Create an integral value from // the string representation. auto Res = llvm::APSInt(template_args[i].m_IntegralValue); Res = Res.extOrTrunc(C.getIntWidth(ArgTy)); @@ -4594,25 +5055,22 @@ TCppScope_t InstantiateTemplate(compat::Interpreter& I, TCppScope_t tmpl, } } - TemplateDecl* TmplD = static_cast(tmpl); + auto* TmplD = unwrap(tmpl); // We will create a new decl, push a transaction. compat::SynthesizingCodeRAII RAII(&getInterp()); return InstantiateTemplate(TmplD, TemplateArgs, S, instantiate_body); } -TCppScope_t InstantiateTemplate(TCppScope_t tmpl, - const TemplateArgInfo* template_args, - size_t template_args_size, - bool instantiate_body) { +DeclRef InstantiateTemplate(DeclRef tmpl, const TemplateArgInfo* template_args, + size_t template_args_size, bool instantiate_body) { INTEROP_TRACE(tmpl, template_args, template_args_size, instantiate_body); return INTEROP_RETURN(InstantiateTemplate( getInterp(), tmpl, template_args, template_args_size, instantiate_body)); } -TCppScope_t -InstantiateTemplate(TCppScope_t tmpl, - const std::vector& template_args, - bool instantiate_body) { +DeclRef InstantiateTemplate(DeclRef tmpl, + const std::vector& template_args, + bool instantiate_body) { INTEROP_TRACE(tmpl, template_args, instantiate_body); // Forward to the static helper directly (not the deprecated public // overload) to avoid a nested INTEROP_TRACE. @@ -4621,10 +5079,10 @@ InstantiateTemplate(TCppScope_t tmpl, template_args.size(), instantiate_body)); } -void GetClassTemplateArgs(TCppScope_t templ_instance, +void GetClassTemplateArgs(ConstDeclRef templ_instance, std::vector& args) { INTEROP_TRACE(templ_instance, INTEROP_OUT(args)); - auto* CTSD = static_cast(templ_instance); + const auto* CTSD = unwrap(templ_instance); for (const auto& TA : CTSD->getTemplateArgs().asArray()) { // FIXME: Support cases with m_IntegralValue. args.push_back({TA.getAsType().getAsOpaquePtr()}); @@ -4632,10 +5090,10 @@ void GetClassTemplateArgs(TCppScope_t templ_instance, return INTEROP_VOID_RETURN(); } -void GetClassTemplateInstantiationArgs(TCppScope_t templ_instance, +void GetClassTemplateInstantiationArgs(ConstDeclRef templ_instance, std::vector& args) { INTEROP_TRACE(templ_instance, INTEROP_OUT(args)); - auto* CTSD = static_cast(templ_instance); + const auto* CTSD = unwrap(templ_instance); for (const auto& TA : CTSD->getTemplateInstantiationArgs().asArray()) { switch (TA.getKind()) { default: @@ -4658,8 +5116,7 @@ void GetClassTemplateInstantiationArgs(TCppScope_t templ_instance, return INTEROP_VOID_RETURN(); } -TCppFunction_t -InstantiateTemplateFunctionFromString(const char* function_template) { +FuncRef InstantiateTemplateFunctionFromString(const char* function_template) { INTEROP_TRACE(function_template); // FIXME: Drop this interface and replace it with the proper overload // resolution handling and template instantiation selection. @@ -4670,29 +5127,29 @@ InstantiateTemplateFunctionFromString(const char* function_template) { std::string instance = "auto " + id + " = " + function_template + ";\n"; if (!Cpp::Declare(instance.c_str(), /*silent=*/false)) { - VarDecl* VD = (VarDecl*)Cpp::GetNamed(id, 0); + auto* VD = unwrap(Cpp::GetNamed(id, nullptr)); DeclRefExpr* DRE = (DeclRefExpr*)VD->getInit()->IgnoreImpCasts(); return INTEROP_RETURN(DRE->getDecl()); } return INTEROP_RETURN(nullptr); } -void GetAllCppNames(TCppScope_t scope, std::set& names) { - INTEROP_TRACE(scope, INTEROP_OUT(names)); - auto* D = (clang::Decl*)scope; +void GetAllCppNames(ConstDeclRef DRef, std::set& names) { + INTEROP_TRACE(DRef, INTEROP_OUT(names)); + const auto* D = unwrap(DRef); clang::DeclContext* DC; clang::DeclContext::decl_iterator decl; compat::SynthesizingCodeRAII RAII(&getInterp()); - if (auto* TD = dyn_cast_or_null(D)) { + if (const auto* TD = dyn_cast_or_null(D)) { DC = clang::TagDecl::castToDeclContext(TD); decl = DC->decls_begin(); decl++; - } else if (auto* ND = dyn_cast_or_null(D)) { + } else if (const auto* ND = dyn_cast_or_null(D)) { DC = clang::NamespaceDecl::castToDeclContext(ND); decl = DC->decls_begin(); - } else if (auto* TUD = dyn_cast_or_null(D)) { + } else if (const auto* TUD = dyn_cast_or_null(D)) { DC = clang::TranslationUnitDecl::castToDeclContext(TUD); decl = DC->decls_begin(); } else { @@ -4700,16 +5157,17 @@ void GetAllCppNames(TCppScope_t scope, std::set& names) { } for (/* decl set above */; decl != DC->decls_end(); decl++) { - if (auto* ND = llvm::dyn_cast_or_null(*decl)) { + if (const auto* ND = llvm::dyn_cast_or_null(*decl)) { names.insert(ND->getNameAsString()); } } return INTEROP_VOID_RETURN(); } -void GetEnums(TCppScope_t scope, std::vector& Result) { - INTEROP_TRACE(scope, INTEROP_OUT(Result)); - auto* D = static_cast(scope); +void GetEnums(ConstDeclRef DRef, std::vector& Result) { + INTEROP_TRACE(DRef, INTEROP_OUT(Result)); + // collectAllContexts is non-const but logically read-only here. + auto* D = const_cast(unwrap(DRef)); if (!llvm::isa_and_nonnull(D)) return INTEROP_VOID_RETURN(); @@ -4730,18 +5188,17 @@ void GetEnums(TCppScope_t scope, std::vector& Result) { return INTEROP_VOID_RETURN(); } -// FIXME: On the CPyCppyy side the receiver is of type -// vector instead of vector -std::vector GetDimensions(TCppType_t type) { - INTEROP_TRACE(type); - QualType Qual = QualType::getFromOpaquePtr(type); +// FIXME: On the CPyCppyy side the receiver is of TyRef +// vector instead of vector +std::vector GetDimensions(ConstTypeRef TyRef) { + INTEROP_TRACE(TyRef); + QualType Qual = QualType::getFromOpaquePtr(TyRef.data); if (Qual.isNull()) return INTEROP_RETURN(std::vector{}); Qual = Qual.getCanonicalType(); std::vector dims; if (Qual->isArrayType()) { - const clang::ArrayType* ArrayType = - dyn_cast(Qual.getTypePtr()); + const auto* ArrayType = dyn_cast(Qual.getTypePtr()); while (ArrayType) { if (const auto* CAT = dyn_cast_or_null(ArrayType)) { llvm::APSInt Size(CAT->getSize()); @@ -4759,33 +5216,33 @@ std::vector GetDimensions(TCppType_t type) { return INTEROP_RETURN(dims); } -bool IsTypeDerivedFrom(TCppType_t derived, TCppType_t base) { +bool IsTypeDerivedFrom(ConstTypeRef derived, ConstTypeRef base) { INTEROP_TRACE(derived, base); auto& S = getSema(); auto fakeLoc = GetValidSLoc(S); - auto derivedType = clang::QualType::getFromOpaquePtr(derived); - auto baseType = clang::QualType::getFromOpaquePtr(base); + auto derivedType = clang::QualType::getFromOpaquePtr(derived.data); + auto baseType = clang::QualType::getFromOpaquePtr(base.data); compat::SynthesizingCodeRAII RAII(&getInterp()); return INTEROP_RETURN(S.IsDerivedFrom(fakeLoc, derivedType, baseType)); } -std::string GetFunctionArgDefault(TCppFunction_t func, - TCppIndex_t param_index) { +std::string GetFunctionArgDefault(ConstFuncRef func, size_t param_index) { INTEROP_TRACE(func, param_index); - auto* D = (clang::Decl*)func; - clang::ParmVarDecl* PI = nullptr; + const auto* D = unwrap(func); + const clang::ParmVarDecl* PI = nullptr; - if (auto* FD = llvm::dyn_cast_or_null(D)) - PI = FD->getParamDecl(param_index); + if (const auto* FD = llvm::dyn_cast_or_null(D)) + PI = FD->getNonObjectParameter(param_index); - else if (auto* FD = llvm::dyn_cast_or_null(D)) - PI = (FD->getTemplatedDecl())->getParamDecl(param_index); + else if (const auto* FD = + llvm::dyn_cast_or_null(D)) + PI = (FD->getTemplatedDecl())->getNonObjectParameter(param_index); if (PI->hasDefaultArg()) { std::string Result; llvm::raw_string_ostream OS(Result); - Expr* DefaultArgExpr = nullptr; + const Expr* DefaultArgExpr = nullptr; compat::SynthesizingCodeRAII RAII(&getInterp()); if (PI->hasUninstantiatedDefaultArg()) DefaultArgExpr = PI->getUninstantiatedDefaultArg(); @@ -4810,27 +5267,28 @@ std::string GetFunctionArgDefault(TCppFunction_t func, return INTEROP_RETURN(""); } -bool IsConstMethod(TCppFunction_t method) { +bool IsConstMethod(ConstFuncRef method) { INTEROP_TRACE(method); if (!method) return INTEROP_RETURN(false); - auto* D = (clang::Decl*)method; - if (auto* func = dyn_cast(D)) + const auto* D = unwrap(method); + if (const auto* func = dyn_cast(D)) return INTEROP_RETURN(func->getMethodQualifiers().hasConst()); return INTEROP_RETURN(false); } -std::string GetFunctionArgName(TCppFunction_t func, TCppIndex_t param_index) { +std::string GetFunctionArgName(ConstFuncRef func, size_t param_index) { INTEROP_TRACE(func, param_index); - auto* D = (clang::Decl*)func; - clang::ParmVarDecl* PI = nullptr; + const auto* D = unwrap(func); + const clang::ParmVarDecl* PI = nullptr; - if (auto* FD = llvm::dyn_cast_or_null(D)) - PI = FD->getParamDecl(param_index); - else if (auto* FD = llvm::dyn_cast_or_null(D)) - PI = (FD->getTemplatedDecl())->getParamDecl(param_index); + if (const auto* FD = llvm::dyn_cast_or_null(D)) + PI = FD->getNonObjectParameter(param_index); + else if (const auto* FD = + llvm::dyn_cast_or_null(D)) + PI = (FD->getTemplatedDecl())->getNonObjectParameter(param_index); return INTEROP_RETURN(PI->getNameAsString()); } @@ -4851,10 +5309,10 @@ Operator GetOperatorFromSpelling(const std::string& op) { return INTEROP_RETURN(Operator::OP_None); } -OperatorArity GetOperatorArity(TCppFunction_t op) { +OperatorArity GetOperatorArity(ConstFuncRef op) { INTEROP_TRACE(op); - Decl* D = static_cast(op); - if (auto* FD = llvm::dyn_cast(D)) { + const auto* D = unwrap(op); + if (const auto* FD = llvm::dyn_cast(D)) { if (FD->isOverloadedOperator()) { switch (FD->getOverloadedOperator()) { #define OVERLOADED_OPERATOR(Name, Spelling, Token, Unary, Binary, MemberOnly) \ @@ -4875,12 +5333,12 @@ OperatorArity GetOperatorArity(TCppFunction_t op) { return INTEROP_RETURN((OperatorArity)~0U); } -void GetOperator(TCppScope_t scope, Operator op, - std::vector& operators, OperatorArity kind) { - INTEROP_TRACE(scope, op, INTEROP_OUT(operators), kind); - Decl* D = static_cast(scope); +void GetOperator(ConstDeclRef DRef, Operator op, + std::vector& operators, OperatorArity kind) { + INTEROP_TRACE(DRef, op, INTEROP_OUT(operators), kind); + const auto* D = unwrap(DRef); compat::SynthesizingCodeRAII RAII(&getInterp()); - if (auto* CXXRD = llvm::dyn_cast_or_null(D)) { + if (const auto* CXXRD = llvm::dyn_cast_or_null(D)) { auto fn = [&operators, kind, op](const RecordDecl* RD) { ASTContext& C = RD->getASTContext(); DeclContextLookupResult Result = @@ -4894,7 +5352,7 @@ void GetOperator(TCppScope_t scope, Operator op, }; fn(CXXRD); CXXRD->forallBases(fn); - } else if (auto* DC = llvm::dyn_cast_or_null(D)) { + } else if (const auto* DC = llvm::dyn_cast_or_null(D)) { ASTContext& C = getSema().getASTContext(); DeclContextLookupResult Result = DC->lookup(C.DeclarationNames.getCXXOperatorName( @@ -4908,33 +5366,34 @@ void GetOperator(TCppScope_t scope, Operator op, return INTEROP_VOID_RETURN(); } -TCppObject_t Allocate(TCppScope_t scope, TCppIndex_t count) { - INTEROP_TRACE(scope, count); - return INTEROP_RETURN( - (TCppObject_t)::operator new(Cpp::SizeOf(scope) * count)); +ObjectRef Allocate(DeclRef DRef, size_t count) { + INTEROP_TRACE(DRef, count); + return INTEROP_RETURN((ObjectRef)::operator new(Cpp::SizeOf(DRef) * count)); } -void Deallocate(TCppScope_t scope, TCppObject_t address, TCppIndex_t count) { - INTEROP_TRACE(scope, address, count); - size_t bytes = Cpp::SizeOf(scope) * count; - ::operator delete(address, bytes); +void Deallocate(DeclRef DRef, ObjectRef address, size_t count) { + INTEROP_TRACE(DRef, address, count); + size_t bytes = Cpp::SizeOf(DRef) * count; + ::operator delete(address.data, bytes); return INTEROP_VOID_RETURN(); } // FIXME: Add optional arguments to the operator new. -TCppObject_t Construct(compat::Interpreter& interp, TCppScope_t scope, - void* arena /*=nullptr*/, TCppIndex_t count /*=1UL*/) { +ObjectRef Construct(compat::Interpreter& interp, DeclRef DRef, + void* arena /*=nullptr*/, size_t count /*=1UL*/) { - if (!Cpp::IsConstructor(scope) && !Cpp::IsClass(scope)) + // DRef may be either a class or a specific constructor declaration. + FuncRef ctorAsFunc = wrap(DRef.data); + if (!Cpp::IsConstructor(ctorAsFunc) && !Cpp::IsClass(DRef)) return nullptr; - if (Cpp::IsClass(scope) && !HasDefaultConstructor(scope)) + if (Cpp::IsClass(DRef) && !HasDefaultConstructor(DRef)) return nullptr; - TCppFunction_t ctor = nullptr; - if (Cpp::IsClass(scope)) - ctor = Cpp::GetDefaultConstructor(scope); + FuncRef ctor = nullptr; + if (Cpp::IsClass(DRef)) + ctor = Cpp::GetDefaultConstructor(DRef); else // a ctor - ctor = scope; + ctor = ctorAsFunc; if (JitCall JC = MakeFunctionCallable(&interp, ctor)) { // invoke the constructor (placement/heap) in one shot @@ -4947,26 +5406,26 @@ TCppObject_t Construct(compat::Interpreter& interp, TCppScope_t scope, return nullptr; } -TCppObject_t Construct(TCppScope_t scope, void* arena /*=nullptr*/, - TCppIndex_t count /*=1UL*/) { - INTEROP_TRACE(scope, arena, count); - return INTEROP_RETURN(Construct(getInterp(), scope, arena, count)); +ObjectRef Construct(DeclRef DRef, void* arena /*=nullptr*/, + size_t count /*=1UL*/) { + INTEROP_TRACE(DRef, arena, count); + return INTEROP_RETURN(Construct(getInterp(), DRef, arena, count)); } -bool Destruct(compat::Interpreter& interp, TCppObject_t This, const Decl* Class, - bool withFree, TCppIndex_t nary) { +bool Destruct(compat::Interpreter& interp, ObjectRef This, const Decl* Class, + bool withFree, size_t nary) { if (auto wrapper = make_dtor_wrapper(interp, Class)) { - (*wrapper)(This, nary, withFree); + (*wrapper)(This.data, nary, withFree); return true; } return false; // FIXME: Enable stronger diagnostics } -bool Destruct(TCppObject_t This, TCppConstScope_t scope, - bool withFree /*=true*/, TCppIndex_t count /*=0UL*/) { - INTEROP_TRACE(This, scope, withFree, count); - const auto* Class = static_cast(scope); +bool Destruct(ObjectRef This, DeclRef DRef, bool withFree /*=true*/, + size_t count /*=0UL*/) { + INTEROP_TRACE(This, DRef, withFree, count); + const auto* Class = unwrap(DRef); return INTEROP_RETURN(Destruct(getInterp(), This, Class, withFree, count)); } @@ -5131,4 +5590,4 @@ int Undo(unsigned N) { #endif } -} // namespace CppImpl +} // namespace Cpp diff --git a/interpreter/CppInterOp/lib/CppInterOp/CppInterOp.td b/interpreter/CppInterOp/lib/CppInterOp/CppInterOp.td index d3958325eae6e..e8a57a441bb53 100644 --- a/interpreter/CppInterOp/lib/CppInterOp/CppInterOp.td +++ b/interpreter/CppInterOp/lib/CppInterOp/CppInterOp.td @@ -24,7 +24,7 @@ services and pushes it onto a stack. adds additional arguments to the interpreter. \returns nullptr on failure.}]; - let ReturnType = "TInterp_t"; + let ReturnType = "InterpRef"; let Args = [ Arg<"const std::vector&", "Args", "{}">, Arg<"const std::vector&", "GpuArgs", "{}"> @@ -37,7 +37,7 @@ Clang-REPL, etcetera). In practice, the selected interpreter should not matter, since the library will function in the same way. \returns the current interpreter instance, if any.}]; - let ReturnType = "TInterp_t"; + let ReturnType = "InterpRef"; } def DeleteInterpreter : CppInterOpAPI { @@ -46,7 +46,7 @@ def DeleteInterpreter : CppInterOpAPI { \returns false on failure or if \c I is not tracked in the stack.}]; let ReturnType = "bool"; - let Args = [Arg<"TInterp_t", "I", "nullptr">]; + let Args = [Arg<"InterpRef", "I", "nullptr">]; } def Process : CppInterOpAPI { @@ -69,19 +69,29 @@ def Declare : CppInterOpAPI { } def Evaluate : CppInterOpAPI { - let Doc = [{Declares, executes and returns the execution result as a intptr_t. + let Doc = [{Declares, executes and returns the execution result as a typed +\c Cpp::Box carrying the Kind tag and the source QualType (when available). +\c getKind() returns \c K_Unspecified on parse error or +no-value-after-success. \param[in] code - the snippet to declare and execute. -\param[out] IsValueInvalid - if non-null, set true when the result is - unusable (parse error or no-Value-after-success). -\returns the expression results as a intptr_t.}]; - - let ReturnType = "intptr_t"; +\returns a \c Cpp::Box. Use \c unbox() to extract a fundamental + (assert-checked: \c T must match the runtime Kind) or + \c getObjectPtr() for object payloads. For unknown-Kind extraction + dispatch on \c getKind() / \c KindOf() first, or use \c visit().}]; + // Box has non-trivial copy/dtor and cannot cross the C ABI; suppress + // the auto-generated C wrapper. + let NoCWrapper = true; + let ReturnType = "Box"; let Args = [ - Arg<"const char*", "code">, - OutArg<"bool*", "IsValueInvalid", "nullptr"> + Arg<"const char*", "code"> ]; } +// NOTE: The legacy C-ABI overload `intptr_t Evaluate(const char*, bool*)` +// and its matching C wrapper `cppinterop_Evaluate` live hand-written in +// CXCppInterOp.cpp / CppInterOp.h (look for "C-ABI"). They are not +// declared here because tablegen does not need to know about them. + // --- Scope / type queries --- def GetScope : CppInterOpAPI { @@ -89,10 +99,10 @@ def GetScope : CppInterOpAPI { passed as a parameter, and if the parent is not passed, then global scope will be assumed.}]; - let ReturnType = "TCppScope_t"; + let ReturnType = "DeclRef"; let Args = [ Arg<"const std::string&", "name">, - Arg<"TCppScope_t", "parent", "nullptr"> + Arg<"ConstDeclRef", "parent", "nullptr"> ]; } @@ -100,31 +110,31 @@ def GetNamed : CppInterOpAPI { let Doc = [{This function performs a lookup within the specified parent, a specific named entity (functions, enums, etcetera).}]; - let ReturnType = "TCppScope_t"; + let ReturnType = "DeclRef"; let Args = [ Arg<"const std::string&", "name">, - Arg<"TCppScope_t", "parent", "nullptr"> + Arg<"ConstDeclRef", "parent", "nullptr"> ]; } def GetGlobalScope : CppInterOpAPI { let Doc = "Gets the global scope of the whole C++ instance."; - let ReturnType = "TCppScope_t"; + let ReturnType = "DeclRef"; } def IsNamespace : CppInterOpAPI { let Doc = "Checks if the scope is a namespace or not."; let ReturnType = "bool"; - let Args = [Arg<"TCppScope_t", "scope">]; + let Args = [Arg<"ConstDeclRef", "DRef">]; } def IsClass : CppInterOpAPI { let Doc = "Checks if the scope is a class or not."; let ReturnType = "bool"; - let Args = [Arg<"TCppScope_t", "scope">]; + let Args = [Arg<"ConstDeclRef", "DRef">]; } def GetType : CppInterOpAPI { @@ -133,29 +143,29 @@ use the name to lookup the actual type. When `parent` is non-null, scoped lookup honours using-directives and type aliases declared inside it; passing `nullptr` (the default) limits lookup to TU scope.}]; - let ReturnType = "TCppType_t"; + let ReturnType = "TypeRef"; let Args = [Arg<"const std::string&", "name">, - Arg<"TCppScope_t", "parent", "nullptr">]; + Arg<"ConstDeclRef", "parent", "nullptr">]; } def GetTypeFromScope : CppInterOpAPI { let Doc = [{This will convert a class into its type, so for example, you can use it to declare variables in it.}]; - let ReturnType = "TCppType_t"; - let Args = [Arg<"TCppScope_t", "klass">]; + let ReturnType = "TypeRef"; + let Args = [Arg<"ConstDeclRef", "DRef">]; } def GetScopeFromType : CppInterOpAPI { let Doc = "Gets the scope of the type that is passed as a parameter."; - let ReturnType = "TCppScope_t"; - let Args = [Arg<"TCppType_t", "type">]; + let ReturnType = "DeclRef"; + let Args = [Arg<"ConstTypeRef", "TyRef">]; } def SizeOf : CppInterOpAPI { let ReturnType = "size_t"; - let Args = [Arg<"TCppScope_t", "scope">]; + let Args = [Arg<"ConstDeclRef", "DRef">]; } // --- Functions with overloads --- @@ -163,15 +173,15 @@ def SizeOf : CppInterOpAPI { def GetFunctionAddress_str : CppInterOpAPI { let Doc = "\\returns the address of the function given its potentially mangled name."; let CppName = "GetFunctionAddress"; - let ReturnType = "TCppFuncAddr_t"; + let ReturnType = "void*"; let Args = [Arg<"const char*", "mangled_name">]; } def GetFunctionAddress_fn : CppInterOpAPI { let Doc = "\\returns the address of the function given its function declaration."; let CppName = "GetFunctionAddress"; - let ReturnType = "TCppFuncAddr_t"; - let Args = [Arg<"TCppFunction_t", "method">]; + let ReturnType = "void*"; + let Args = [Arg<"FuncRef", "method">]; } // --- Construction / destruction --- @@ -179,18 +189,18 @@ def GetFunctionAddress_fn : CppInterOpAPI { def Construct : CppInterOpAPI { let Doc = [{Creates one or more objects of class \c scope by calling its default constructor. -\param[in] scope Class to construct, or handle to Constructor +\param[in] scope Class to construct, or DRef to Constructor \param[in] arena If set, this API uses placement new to construct at this address. \param[in] is used to indicate the number of objects to construct. \returns a pointer to the constructed object, which is arena if placement new is used.}]; - let ReturnType = "TCppObject_t"; + let ReturnType = "ObjectRef"; let Args = [ - Arg<"TCppScope_t", "scope">, + Arg<"DeclRef", "DRef">, Arg<"void*", "arena", "nullptr">, - Arg<"TCppIndex_t", "count", "1UL"> + Arg<"size_t", "count", "1UL"> ]; } @@ -207,10 +217,10 @@ points to an array of objects let ReturnType = "bool"; let Args = [ - Arg<"TCppObject_t", "This">, - Arg<"TCppConstScope_t", "scope">, + Arg<"ObjectRef", "This">, + Arg<"DeclRef", "DRef">, Arg<"bool", "withFree", "true">, - Arg<"TCppIndex_t", "count", "0UL"> + Arg<"size_t", "count", "0UL"> ]; } @@ -249,7 +259,7 @@ def ObjToString : CppInterOpAPI { let ReturnType = "std::string"; let Args = [ - Arg<"const char*", "type">, + Arg<"const char*", "TyRef">, Arg<"void*", "obj"> ]; } @@ -259,16 +269,16 @@ def GetValueKind : CppInterOpAPI { let ReturnType = "ValueKind"; let Args = [ - Arg<"TCppType_t", "type"> + Arg<"ConstTypeRef", "TyRef"> ]; } def GetNonReferenceType : CppInterOpAPI { let Doc = "Get the type that the reference refers to"; - let ReturnType = "TCppType_t"; + let ReturnType = "TypeRef"; let Args = [ - Arg<"TCppType_t", "type"> + Arg<"ConstTypeRef", "TyRef"> ]; } @@ -277,7 +287,7 @@ def IsEnumType : CppInterOpAPI { let ReturnType = "bool"; let Args = [ - Arg<"TCppType_t", "type"> + Arg<"ConstTypeRef", "TyRef"> ]; } @@ -286,7 +296,7 @@ def GetLanguage : CppInterOpAPI { let ReturnType = "InterpreterLanguage"; let Args = [ - Arg<"TInterp_t", "I", "nullptr"> + Arg<"InterpRef", "I", "nullptr"> ]; } @@ -295,16 +305,16 @@ def GetLanguageStandard : CppInterOpAPI { let ReturnType = "InterpreterLanguageStandard"; let Args = [ - Arg<"TInterp_t", "I", "nullptr"> + Arg<"InterpRef", "I", "nullptr"> ]; } def GetReferencedType : CppInterOpAPI { let Doc = "Get lvalue referenced type, or rvalue if rvalue is true"; - let ReturnType = "TCppType_t"; + let ReturnType = "TypeRef"; let Args = [ - Arg<"TCppType_t", "type">, + Arg<"ConstTypeRef", "TyRef">, Arg<"bool", "rvalue", "false"> ]; } @@ -314,25 +324,25 @@ def IsPointerType : CppInterOpAPI { let ReturnType = "bool"; let Args = [ - Arg<"TCppType_t", "type"> + Arg<"ConstTypeRef", "TyRef"> ]; } def GetPointeeType : CppInterOpAPI { let Doc = "Get the underlying pointee type"; - let ReturnType = "TCppType_t"; + let ReturnType = "TypeRef"; let Args = [ - Arg<"TCppType_t", "type"> + Arg<"ConstTypeRef", "TyRef"> ]; } def GetPointerType : CppInterOpAPI { let Doc = "Get the pointer to type"; - let ReturnType = "TCppType_t"; + let ReturnType = "TypeRef"; let Args = [ - Arg<"TCppType_t", "type"> + Arg<"ConstTypeRef", "TyRef"> ]; } @@ -341,7 +351,7 @@ def IsReferenceType : CppInterOpAPI { let ReturnType = "bool"; let Args = [ - Arg<"TCppType_t", "type"> + Arg<"ConstTypeRef", "TyRef"> ]; } @@ -350,7 +360,7 @@ def GetTypeAsString : CppInterOpAPI { let ReturnType = "std::string"; let Args = [ - Arg<"TCppType_t", "type"> + Arg<"ConstTypeRef", "TyRef"> ]; } @@ -359,9 +369,9 @@ def GetCanonicalType : CppInterOpAPI { is the type with any typedef names, syntactic sugars or modifiers stripped out of it.}]; - let ReturnType = "TCppType_t"; + let ReturnType = "TypeRef"; let Args = [ - Arg<"TCppType_t", "type"> + Arg<"ConstTypeRef", "TyRef"> ]; } @@ -371,7 +381,7 @@ qual can be ORed value of enum QualKind}]; let ReturnType = "bool"; let Args = [ - Arg<"TCppType_t", "type">, + Arg<"ConstTypeRef", "TyRef">, Arg<"QualKind", "qual"> ]; } @@ -380,9 +390,9 @@ def RemoveTypeQualifier : CppInterOpAPI { let Doc = [{Returns type with the qual Qualifiers removed qual can be ORed value of enum QualKind}]; - let ReturnType = "TCppType_t"; + let ReturnType = "TypeRef"; let Args = [ - Arg<"TCppType_t", "type">, + Arg<"ConstTypeRef", "TyRef">, Arg<"QualKind", "qual"> ]; } @@ -390,9 +400,9 @@ qual can be ORed value of enum QualKind}]; def GetUnderlyingType : CppInterOpAPI { let Doc = "Gets the pure, Underlying Type (as opposed to the Using Type)."; - let ReturnType = "TCppType_t"; + let ReturnType = "TypeRef"; let Args = [ - Arg<"TCppType_t", "type"> + Arg<"ConstTypeRef", "TyRef"> ]; } @@ -401,25 +411,25 @@ def IsRecordType : CppInterOpAPI { let ReturnType = "bool"; let Args = [ - Arg<"TCppType_t", "type"> + Arg<"ConstTypeRef", "TyRef"> ]; } def GetVariableType : CppInterOpAPI { let Doc = "Gets the type of the variable that is passed as a parameter."; - let ReturnType = "TCppType_t"; + let ReturnType = "TypeRef"; let Args = [ - Arg<"TCppScope_t", "var"> + Arg<"ConstDeclRef", "var"> ]; } def GetComplexType : CppInterOpAPI { let Doc = "\\returns the complex of the provided type."; - let ReturnType = "TCppType_t"; + let ReturnType = "TypeRef"; let Args = [ - Arg<"TCppType_t", "element_type"> + Arg<"ConstTypeRef", "element_type"> ]; } @@ -427,9 +437,9 @@ def GetUnderlyingScope : CppInterOpAPI { let Doc = [{Strips the typedef and returns the underlying class, and if the underlying decl is not a class it returns the input unchanged.}]; - let ReturnType = "TCppScope_t"; + let ReturnType = "DeclRef"; let Args = [ - Arg<"TCppScope_t", "scope"> + Arg<"ConstDeclRef", "DRef"> ]; } @@ -447,9 +457,9 @@ variable templates \returns Instantiated templated class/function/variable pointer}]; - let ReturnType = "TCppScope_t"; + let ReturnType = "DeclRef"; let Args = [ - Arg<"TCppScope_t", "tmpl">, + Arg<"DeclRef", "tmpl">, Arg<"const TemplateArgInfo*", "template_args">, Arg<"size_t", "template_args_size">, Arg<"bool", "instantiate_body", "false"> @@ -464,9 +474,9 @@ pointer+size overload with \c template_args.data() and \c template_args.size().}]; let CppName = "InstantiateTemplate"; - let ReturnType = "TCppScope_t"; + let ReturnType = "DeclRef"; let Args = [ - Arg<"TCppScope_t", "tmpl">, + Arg<"DeclRef", "tmpl">, Arg<"const std::vector&", "template_args">, Arg<"bool", "instantiate_body", "false"> ]; @@ -475,9 +485,9 @@ pointer+size overload with \c template_args.data() and def GetParentScope : CppInterOpAPI { let Doc = "Gets the parent of the scope that is passed as a parameter."; - let ReturnType = "TCppScope_t"; + let ReturnType = "DeclRef"; let Args = [ - Arg<"TCppScope_t", "scope"> + Arg<"ConstDeclRef", "DRef"> ]; } @@ -486,16 +496,16 @@ def IsTemplate : CppInterOpAPI { let ReturnType = "bool"; let Args = [ - Arg<"TCppScope_t", "handle"> + Arg<"ConstDeclRef", "DRef"> ]; } def IsTypedefed : CppInterOpAPI { - let Doc = "Checks if \\c handle introduces a typedef name via \\c typedef or \\c using."; + let Doc = "Checks if \\c DRef introduces a typedef name via \\c typedef or \\c using."; let ReturnType = "bool"; let Args = [ - Arg<"TCppScope_t", "handle"> + Arg<"ConstDeclRef", "DRef"> ]; } @@ -505,7 +515,7 @@ which means that the class contains or inherits a virtual function}]; let ReturnType = "bool"; let Args = [ - Arg<"TCppScope_t", "klass"> + Arg<"ConstDeclRef", "DRef"> ]; } @@ -523,7 +533,7 @@ def GetSizeOfType : CppInterOpAPI { let ReturnType = "size_t"; let Args = [ - Arg<"TCppType_t", "type"> + Arg<"ConstTypeRef", "TyRef"> ]; } @@ -532,7 +542,7 @@ def IsBuiltin : CppInterOpAPI { let ReturnType = "bool"; let Args = [ - Arg<"TCppConstType_t", "type"> + Arg<"ConstTypeRef", "TyRef"> ]; } @@ -542,7 +552,7 @@ template instantiation if necessary.}]; let ReturnType = "bool"; let Args = [ - Arg<"TCppScope_t", "scope"> + Arg<"ConstDeclRef", "DRef"> ]; } @@ -551,10 +561,10 @@ def Allocate : CppInterOpAPI { \param[in] scope Given class for which to allocate memory for \param[in] count is used to indicate the number of objects to allocate for.}]; - let ReturnType = "TCppObject_t"; + let ReturnType = "ObjectRef"; let Args = [ - Arg<"TCppScope_t", "scope">, - Arg<"TCppIndex_t", "count", "1UL"> + Arg<"DeclRef", "DRef">, + Arg<"size_t", "count", "1UL"> ]; } @@ -565,16 +575,16 @@ def Deallocate : CppInterOpAPI { let ReturnType = "void"; let Args = [ - Arg<"TCppScope_t", "scope">, - Arg<"TCppObject_t", "address">, - Arg<"TCppIndex_t", "count", "1UL"> + Arg<"DeclRef", "DRef">, + Arg<"ObjectRef", "address">, + Arg<"size_t", "count", "1UL"> ]; } def IsAbstract : CppInterOpAPI { let ReturnType = "bool"; let Args = [ - Arg<"TCppType_t", "klass"> + Arg<"ConstDeclRef", "DRef"> ]; } @@ -583,7 +593,7 @@ def IsEnumScope : CppInterOpAPI { let ReturnType = "bool"; let Args = [ - Arg<"TCppScope_t", "handle"> + Arg<"ConstDeclRef", "DRef"> ]; } @@ -593,7 +603,7 @@ each enum constant that is defined).}]; let ReturnType = "bool"; let Args = [ - Arg<"TCppScope_t", "handle"> + Arg<"ConstDeclRef", "DRef"> ]; } @@ -605,7 +615,7 @@ initialization.}]; let ReturnType = "bool"; let Args = [ - Arg<"TCppScope_t", "scope"> + Arg<"ConstDeclRef", "DRef"> ]; } @@ -614,14 +624,14 @@ def IsVariable : CppInterOpAPI { let ReturnType = "bool"; let Args = [ - Arg<"TCppScope_t", "scope"> + Arg<"ConstDeclRef", "DRef"> ]; } def GetAllCppNames : CppInterOpAPI { let ReturnType = "void"; let Args = [ - Arg<"TCppScope_t", "scope">, + Arg<"ConstDeclRef", "DRef">, OutArg<"std::set&", "names"> ]; } @@ -629,9 +639,9 @@ def GetAllCppNames : CppInterOpAPI { def GetUsingNamespaces : CppInterOpAPI { let Doc = "Gets the list of namespaces utilized in the supplied scope."; - let ReturnType = "std::vector"; + let ReturnType = "std::vector"; let Args = [ - Arg<"TCppScope_t", "scope"> + Arg<"ConstDeclRef", "DRef"> ]; } @@ -641,16 +651,16 @@ the name, it also gets the template arguments.}]; let ReturnType = "std::string"; let Args = [ - Arg<"TCppScope_t", "klass"> + Arg<"ConstDeclRef", "DRef"> ]; } def GetDestructor : CppInterOpAPI { let Doc = "\\returns the class destructor, if any."; - let ReturnType = "TCppFunction_t"; + let ReturnType = "FuncRef"; let Args = [ - Arg<"TCppScope_t", "scope"> + Arg<"ConstDeclRef", "DRef"> ]; } @@ -659,7 +669,7 @@ def IsVirtualMethod : CppInterOpAPI { let ReturnType = "bool"; let Args = [ - Arg<"TCppFunction_t", "method"> + Arg<"ConstFuncRef", "method"> ]; } @@ -667,9 +677,9 @@ def GetNumBases : CppInterOpAPI { let Doc = [{Gets the number of Base Classes for the Derived Class that is passed as a parameter.}]; - let ReturnType = "TCppIndex_t"; + let ReturnType = "size_t"; let Args = [ - Arg<"TCppScope_t", "klass"> + Arg<"ConstDeclRef", "DRef"> ]; } @@ -679,7 +689,7 @@ namespace, variable, or a function).}]; let ReturnType = "std::string"; let Args = [ - Arg<"TCppScope_t", "klass"> + Arg<"ConstDeclRef", "DRef"> ]; } @@ -689,10 +699,10 @@ is used to get the number of Base Classes, and then that number can be used to iterate through the index value to get each specific base class.}]; - let ReturnType = "TCppScope_t"; + let ReturnType = "DeclRef"; let Args = [ - Arg<"TCppScope_t", "klass">, - Arg<"TCppIndex_t", "ibase"> + Arg<"ConstDeclRef", "DRef">, + Arg<"size_t", "ibase"> ]; } @@ -702,8 +712,8 @@ provided Base Class.}]; let ReturnType = "bool"; let Args = [ - Arg<"TCppScope_t", "derived">, - Arg<"TCppScope_t", "base"> + Arg<"ConstDeclRef", "derived">, + Arg<"ConstDeclRef", "base"> ]; } @@ -712,9 +722,9 @@ def GetOperator : CppInterOpAPI { let ReturnType = "void"; let Args = [ - Arg<"TCppScope_t", "scope">, + Arg<"ConstDeclRef", "DRef">, Arg<"Operator", "op">, - OutArg<"std::vector&", "operators">, + OutArg<"std::vector&", "operators">, Arg<"OperatorArity", "kind", "kBoth"> ]; } @@ -725,8 +735,8 @@ used to get to the Base Class fields.}]; let ReturnType = "int64_t"; let Args = [ - Arg<"TCppScope_t", "derived">, - Arg<"TCppScope_t", "base"> + Arg<"ConstDeclRef", "derived">, + Arg<"ConstDeclRef", "base"> ]; } @@ -739,8 +749,8 @@ supplied as a parameter. let ReturnType = "void"; let Args = [ - Arg<"TCppScope_t", "klass">, - OutArg<"std::vector&", "methods"> + Arg<"ConstDeclRef", "DRef">, + OutArg<"std::vector&", "methods"> ]; } @@ -749,7 +759,7 @@ def IsFunctionProtoType : CppInterOpAPI { let ReturnType = "bool"; let Args = [ - Arg<"TCppType_t", "typ"> + Arg<"ConstTypeRef", "TyRef"> ]; } @@ -759,17 +769,17 @@ def GetFnTypeSignature : CppInterOpAPI { let ReturnType = "void"; let Args = [ - Arg<"TCppType_t", "fn_type">, - OutArg<"std::vector&", "sig"> + Arg<"ConstTypeRef", "fn_type">, + OutArg<"std::vector&", "sig"> ]; } def GetFunctionNumArgs : CppInterOpAPI { let Doc = "Gets the number of Arguments for the provided function."; - let ReturnType = "TCppIndex_t"; + let ReturnType = "size_t"; let Args = [ - Arg<"TCppFunction_t", "func"> + Arg<"ConstFuncRef", "func"> ]; } @@ -778,8 +788,8 @@ def GetFunctionArgName : CppInterOpAPI { let ReturnType = "std::string"; let Args = [ - Arg<"TCppFunction_t", "func">, - Arg<"TCppIndex_t", "param_index"> + Arg<"ConstFuncRef", "func">, + Arg<"size_t", "param_index"> ]; } @@ -788,10 +798,10 @@ def GetFunctionArgType : CppInterOpAPI { by providing the Argument Index, based on the number of arguments from the GetFunctionNumArgs() function.}]; - let ReturnType = "TCppType_t"; + let ReturnType = "TypeRef"; let Args = [ - Arg<"TCppFunction_t", "func">, - Arg<"TCppIndex_t", "iarg"> + Arg<"ConstFuncRef", "func">, + Arg<"size_t", "iarg"> ]; } @@ -800,14 +810,14 @@ def IsConstMethod : CppInterOpAPI { let ReturnType = "bool"; let Args = [ - Arg<"TCppFunction_t", "method"> + Arg<"ConstFuncRef", "method"> ]; } def IsTemplatedFunction : CppInterOpAPI { let ReturnType = "bool"; let Args = [ - Arg<"TCppFunction_t", "func"> + Arg<"ConstFuncRef", "func"> ]; } @@ -816,7 +826,7 @@ def IsStaticMethod : CppInterOpAPI { let ReturnType = "bool"; let Args = [ - Arg<"TCppConstFunction_t", "method"> + Arg<"ConstFuncRef", "method"> ]; } @@ -825,7 +835,7 @@ def IsFunctionDeleted : CppInterOpAPI { let ReturnType = "bool"; let Args = [ - Arg<"TCppConstFunction_t", "function"> + Arg<"ConstFuncRef", "function"> ]; } @@ -834,7 +844,7 @@ def IsPublicMethod : CppInterOpAPI { let ReturnType = "bool"; let Args = [ - Arg<"TCppFunction_t", "method"> + Arg<"ConstFuncRef", "method"> ]; } @@ -843,7 +853,7 @@ def IsProtectedMethod : CppInterOpAPI { let ReturnType = "bool"; let Args = [ - Arg<"TCppFunction_t", "method"> + Arg<"ConstFuncRef", "method"> ]; } @@ -852,7 +862,7 @@ def IsPrivateMethod : CppInterOpAPI { let ReturnType = "bool"; let Args = [ - Arg<"TCppFunction_t", "method"> + Arg<"ConstFuncRef", "method"> ]; } @@ -861,7 +871,7 @@ def IsConstructor : CppInterOpAPI { let ReturnType = "bool"; let Args = [ - Arg<"TCppConstFunction_t", "method"> + Arg<"ConstFuncRef", "method"> ]; } @@ -870,7 +880,7 @@ def IsDestructor : CppInterOpAPI { let ReturnType = "bool"; let Args = [ - Arg<"TCppConstFunction_t", "method"> + Arg<"ConstFuncRef", "method"> ]; } @@ -879,8 +889,8 @@ def GetDatamembers : CppInterOpAPI { let ReturnType = "void"; let Args = [ - Arg<"TCppScope_t", "scope">, - OutArg<"std::vector&", "datamembers"> + Arg<"DeclRef", "DRef">, + OutArg<"std::vector&", "datamembers"> ]; } @@ -891,18 +901,18 @@ def GetStaticDatamembers : CppInterOpAPI { let ReturnType = "void"; let Args = [ - Arg<"TCppScope_t", "scope">, - OutArg<"std::vector&", "datamembers"> + Arg<"ConstDeclRef", "DRef">, + OutArg<"std::vector&", "datamembers"> ]; } def LookupDatamember : CppInterOpAPI { let Doc = "This is a Lookup function to be used specifically for data members."; - let ReturnType = "TCppScope_t"; + let ReturnType = "DeclRef"; let Args = [ Arg<"const std::string&", "name">, - Arg<"TCppScope_t", "parent"> + Arg<"ConstDeclRef", "parent"> ]; } @@ -911,7 +921,7 @@ def IsLambdaClass : CppInterOpAPI { let ReturnType = "bool"; let Args = [ - Arg<"TCppType_t", "type"> + Arg<"ConstTypeRef", "TyRef"> ]; } @@ -921,7 +931,7 @@ named decl (a class, namespace, variable, or a function).}]; let ReturnType = "std::string"; let Args = [ - Arg<"TCppScope_t", "klass"> + Arg<"ConstDeclRef", "DRef"> ]; } @@ -931,8 +941,8 @@ value stored in the variable.}]; let ReturnType = "intptr_t"; let Args = [ - Arg<"TCppScope_t", "var">, - Arg<"TCppScope_t", "parent", "nullptr"> + Arg<"ConstDeclRef", "var">, + Arg<"ConstDeclRef", "parent", "nullptr"> ]; } @@ -941,7 +951,7 @@ def IsPublicVariable : CppInterOpAPI { let ReturnType = "bool"; let Args = [ - Arg<"TCppScope_t", "var"> + Arg<"ConstDeclRef", "var"> ]; } @@ -950,7 +960,7 @@ def IsProtectedVariable : CppInterOpAPI { let ReturnType = "bool"; let Args = [ - Arg<"TCppScope_t", "var"> + Arg<"ConstDeclRef", "var"> ]; } @@ -959,7 +969,7 @@ def IsPrivateVariable : CppInterOpAPI { let ReturnType = "bool"; let Args = [ - Arg<"TCppScope_t", "var"> + Arg<"ConstDeclRef", "var"> ]; } @@ -968,7 +978,7 @@ def IsStaticVariable : CppInterOpAPI { let ReturnType = "bool"; let Args = [ - Arg<"TCppScope_t", "var"> + Arg<"ConstDeclRef", "var"> ]; } @@ -977,7 +987,7 @@ def IsConstVariable : CppInterOpAPI { let ReturnType = "bool"; let Args = [ - Arg<"TCppScope_t", "var"> + Arg<"ConstDeclRef", "var"> ]; } @@ -987,25 +997,25 @@ def GetDimensions : CppInterOpAPI { let NoCWrapper = true; let ReturnType = "std::vector"; let Args = [ - Arg<"TCppType_t", "type"> + Arg<"ConstTypeRef", "TyRef"> ]; } def GetEnumConstants : CppInterOpAPI { let Doc = "Gets a list of all the enum constants for an enum."; - let ReturnType = "std::vector"; + let ReturnType = "std::vector"; let Args = [ - Arg<"TCppScope_t", "scope"> + Arg<"ConstDeclRef", "DRef"> ]; } def GetEnumConstantType : CppInterOpAPI { let Doc = "Gets the enum name when an enum constant is passed."; - let ReturnType = "TCppType_t"; + let ReturnType = "TypeRef"; let Args = [ - Arg<"TCppScope_t", "scope"> + Arg<"ConstDeclRef", "DRef"> ]; } @@ -1013,16 +1023,16 @@ def GetEnumConstantValue : CppInterOpAPI { let Doc = [{Gets the index value (0,1,2, etcetera) of the enum constant that was passed into this function.}]; - let ReturnType = "TCppIndex_t"; + let ReturnType = "size_t"; let Args = [ - Arg<"TCppScope_t", "scope"> + Arg<"ConstDeclRef", "DRef"> ]; } def DumpScope : CppInterOpAPI { let ReturnType = "void"; let Args = [ - Arg<"TCppScope_t", "scope"> + Arg<"ConstDeclRef", "DRef"> ]; } @@ -1065,7 +1075,7 @@ def GetDoxygenComment : CppInterOpAPI { let ReturnType = "std::string"; let Args = [ - Arg<"TCppScope_t", "scope">, + Arg<"ConstDeclRef", "DRef">, Arg<"bool", "strip_comment_markers", "true"> ]; } @@ -1075,7 +1085,7 @@ def IsExplicit : CppInterOpAPI { let ReturnType = "bool"; let Args = [ - Arg<"TCppConstFunction_t", "method"> + Arg<"ConstFuncRef", "method"> ]; } @@ -1083,18 +1093,18 @@ def IsExplicit : CppInterOpAPI { // --- Functions not previously in the dispatch table --- def ActivateInterpreter : CppInterOpAPI { - let Doc = [{Activates an instance of an interpreter to handle subsequent API + let Doc = [{Activates an instance of an interpreter to DRef subsequent API requests.}]; let ReturnType = "bool"; - let Args = [Arg<"TInterp_t", "I">]; + let Args = [Arg<"InterpRef", "I">]; } def AddTypeQualifier : CppInterOpAPI { let Doc = [{Returns type with the qual Qualifiers added. qual can be ORed value of enum QualKind.}]; - let ReturnType = "TCppType_t"; + let ReturnType = "TypeRef"; let Args = [ - Arg<"TCppType_t", "type">, + Arg<"ConstTypeRef", "TyRef">, Arg<"QualKind", "qual"> ]; } @@ -1109,9 +1119,9 @@ def BeginStdStreamCapture : CppInterOpAPI { def BestOverloadFunctionMatch : CppInterOpAPI { let Doc = [{Finds best overload match based on explicit template parameters (if any) and argument types.}]; - let ReturnType = "TCppFunction_t"; + let ReturnType = "FuncRef"; let Args = [ - Arg<"const std::vector&", "candidates">, + Arg<"const std::vector&", "candidates">, Arg<"const std::vector&", "explicit_types">, Arg<"const std::vector&", "arg_types"> ]; @@ -1162,7 +1172,7 @@ templated function of that type within the parent scope.}]; let ReturnType = "bool"; let Args = [ Arg<"const std::string&", "name">, - Arg<"TCppScope_t", "parent", "nullptr"> + Arg<"ConstDeclRef", "parent", "nullptr"> ]; } @@ -1172,7 +1182,7 @@ def GetClassTemplateArgs : CppInterOpAPI { \param[out] args Vector of instantiation arguments.}]; let ReturnType = "void"; let Args = [ - Arg<"TCppScope_t", "templ_instance">, + Arg<"ConstDeclRef", "templ_instance">, OutArg<"std::vector&", "args"> ]; } @@ -1185,7 +1195,7 @@ def GetClassTemplateInstantiationArgs : CppInterOpAPI { let NoCWrapper = true; let ReturnType = "void"; let Args = [ - Arg<"TCppScope_t", "templ_instance">, + Arg<"ConstDeclRef", "templ_instance">, OutArg<"std::vector&", "args"> ]; } @@ -1198,15 +1208,15 @@ that is supplied as a parameter.}]; let ReturnType = "bool"; let Args = [ Arg<"const std::string&", "name">, - Arg<"TCppScope_t", "parent">, - OutArg<"std::vector&", "funcs"> + Arg<"ConstDeclRef", "parent">, + OutArg<"std::vector&", "funcs"> ]; } def GetDefaultConstructor : CppInterOpAPI { let Doc = "\\returns the default constructor of a class, if any."; - let ReturnType = "TCppFunction_t"; - let Args = [Arg<"TCppScope_t", "scope">]; + let ReturnType = "FuncRef"; + let Args = [Arg<"DeclRef", "DRef">]; } def GetEnumConstantDatamembers : CppInterOpAPI { @@ -1216,8 +1226,8 @@ def GetEnumConstantDatamembers : CppInterOpAPI { \param[in] include_enum_class Whether to include enum class members.}]; let ReturnType = "void"; let Args = [ - Arg<"TCppScope_t", "scope">, - OutArg<"std::vector&", "datamembers">, + Arg<"ConstDeclRef", "DRef">, + OutArg<"std::vector&", "datamembers">, Arg<"bool", "include_enum_class", "true"> ]; } @@ -1226,7 +1236,7 @@ def GetEnums : CppInterOpAPI { let Doc = "Extracts enum declarations from a specified scope and stores them in a vector."; let ReturnType = "void"; let Args = [ - Arg<"TCppScope_t", "scope">, + Arg<"ConstDeclRef", "DRef">, OutArg<"std::vector&", "Result"> ]; } @@ -1235,28 +1245,28 @@ def GetFunctionArgDefault : CppInterOpAPI { let Doc = "\\returns the default argument value as string."; let ReturnType = "std::string"; let Args = [ - Arg<"TCppFunction_t", "func">, - Arg<"TCppIndex_t", "param_index"> + Arg<"ConstFuncRef", "func">, + Arg<"size_t", "param_index"> ]; } def GetFunctionRequiredArgs : CppInterOpAPI { let Doc = "Gets the number of Required Arguments for the provided function."; - let ReturnType = "TCppIndex_t"; - let Args = [Arg<"TCppConstFunction_t", "func">]; + let ReturnType = "size_t"; + let Args = [Arg<"ConstFuncRef", "func">]; } def GetFunctionReturnType : CppInterOpAPI { let Doc = "Gets the return type of the provided function."; - let ReturnType = "TCppType_t"; - let Args = [Arg<"TCppFunction_t", "func">]; + let ReturnType = "TypeRef"; + let Args = [Arg<"ConstFuncRef", "func">]; } def GetFunctionSignature : CppInterOpAPI { let Doc = [{\\returns a stringified version of a given function signature in the form: void N::f(int i, double d, long l = 0, char ch = 'a').}]; let ReturnType = "std::string"; - let Args = [Arg<"TCppFunction_t", "func">]; + let Args = [Arg<"ConstFuncRef", "func">]; } def GetFunctionTemplatedDecls : CppInterOpAPI { @@ -1264,17 +1274,17 @@ def GetFunctionTemplatedDecls : CppInterOpAPI { un-instantiated/non-overloaded templated methods.}]; let ReturnType = "void"; let Args = [ - Arg<"TCppScope_t", "klass">, - OutArg<"std::vector&", "methods"> + Arg<"ConstDeclRef", "DRef">, + OutArg<"std::vector&", "methods"> ]; } def GetFunctionsUsingName : CppInterOpAPI { let Doc = [{Looks up all the functions that have the name that is passed as a parameter in this function.}]; - let ReturnType = "std::vector"; + let ReturnType = "std::vector"; let Args = [ - Arg<"TCppScope_t", "scope">, + Arg<"ConstDeclRef", "DRef">, Arg<"const std::string&", "name"> ]; } @@ -1294,21 +1304,21 @@ def GetIncludePaths : CppInterOpAPI { def GetIntegerTypeFromEnumScope : CppInterOpAPI { let Doc = [{For the given "class", get the integer type that the enum represents, so that you can store it properly in a box.}]; - let ReturnType = "TCppType_t"; - let Args = [Arg<"TCppScope_t", "handle">]; + let ReturnType = "TypeRef"; + let Args = [Arg<"ConstDeclRef", "DRef">]; } def GetIntegerTypeFromEnumType : CppInterOpAPI { let Doc = [{For the given "type", this function gets the integer type that the enum represents.}]; - let ReturnType = "TCppType_t"; - let Args = [Arg<"TCppType_t", "handle">]; + let ReturnType = "TypeRef"; + let Args = [Arg<"ConstTypeRef", "DRef">]; } def GetOperatorArity : CppInterOpAPI { let Doc = "\\returns arity of the operator or kNone."; let ReturnType = "OperatorArity"; - let Args = [Arg<"TCppFunction_t", "op">]; + let Args = [Arg<"ConstFuncRef", "op">]; } def GetOperatorFromSpelling : CppInterOpAPI { @@ -1321,14 +1331,14 @@ def GetQualifiedCompleteName : CppInterOpAPI { let Doc = [{This is similar to GetQualifiedName() function, but besides the "qualified" name, it also gets the template arguments.}]; let ReturnType = "std::string"; - let Args = [Arg<"TCppScope_t", "klass">]; + let Args = [Arg<"ConstDeclRef", "DRef">]; } def GetScopeFromCompleteName : CppInterOpAPI { let Doc = [{When the namespace is known, then the parent doesn't need to be specified. This will probably be phased-out in future versions of the interop library.}]; - let ReturnType = "TCppScope_t"; + let ReturnType = "DeclRef"; let Args = [Arg<"const std::string&", "name">]; } @@ -1355,7 +1365,7 @@ reports so a reviewer can compare their build to the reporter's.}]; def HasDefaultConstructor : CppInterOpAPI { let Doc = "\\returns if a class has a default constructor."; let ReturnType = "bool"; - let Args = [Arg<"TCppScope_t", "scope">]; + let Args = [Arg<"ConstDeclRef", "DRef">]; } def InsertOrReplaceJitSymbol : CppInterOpAPI { @@ -1371,53 +1381,61 @@ This is useful for providing implementations for symbols not linked in.}]; def InstantiateTemplateFunctionFromString : CppInterOpAPI { let Doc = [{Instantiates a function template from a given string representation. This function parses and instantiates a template function.}]; - let ReturnType = "TCppFunction_t"; + let ReturnType = "FuncRef"; let Args = [Arg<"const char*", "function_template">]; } +def IsTemplateParmType : CppInterOpAPI { + let Doc = "Checks if the given type is a template parameter type."; + let ReturnType = "bool"; + let Args = [ + Arg<"ConstTypeRef", "TyRef"> + ]; +} + def IsFunction : CppInterOpAPI { let Doc = "Checks if the scope is a function."; let ReturnType = "bool"; - let Args = [Arg<"TCppScope_t", "scope">]; + let Args = [Arg<"ConstDeclRef", "DRef">]; } def IsFunctionPointerType : CppInterOpAPI { let Doc = "Checks if the type is a function pointer."; let ReturnType = "bool"; - let Args = [Arg<"TCppType_t", "type">]; + let Args = [Arg<"ConstTypeRef", "TyRef">]; } def IsMethod : CppInterOpAPI { let Doc = "Checks if the provided parameter is a method."; let ReturnType = "bool"; - let Args = [Arg<"TCppConstFunction_t", "method">]; + let Args = [Arg<"ConstFuncRef", "method">]; } def IsPODType : CppInterOpAPI { let Doc = "Checks if the provided parameter is a Plain Old Data Type (POD)."; let ReturnType = "bool"; - let Args = [Arg<"TCppType_t", "type">]; + let Args = [Arg<"ConstTypeRef", "TyRef">]; } def IsSmartPtrType : CppInterOpAPI { let Doc = [{We assume that smart pointer types define both operator* and operator->.}]; let ReturnType = "bool"; - let Args = [Arg<"TCppType_t", "type">]; + let Args = [Arg<"ConstTypeRef", "TyRef">]; } def IsTemplateSpecialization : CppInterOpAPI { let Doc = "Checks if it is a class template specialization class."; let ReturnType = "bool"; - let Args = [Arg<"TCppScope_t", "handle">]; + let Args = [Arg<"ConstDeclRef", "DRef">]; } def IsTypeDerivedFrom : CppInterOpAPI { let Doc = "Checks if a C++ type derives from another."; let ReturnType = "bool"; let Args = [ - Arg<"TCppType_t", "derived">, - Arg<"TCppType_t", "base"> + Arg<"ConstTypeRef", "derived">, + Arg<"ConstTypeRef", "base"> ]; } @@ -1427,8 +1445,8 @@ supplied as a parameter.}]; let ReturnType = "void"; let Args = [ Arg<"const std::string&", "name">, - Arg<"TCppScope_t", "parent">, - OutArg<"std::vector&", "funcs"> + Arg<"ConstDeclRef", "parent">, + OutArg<"std::vector&", "funcs"> ]; } @@ -1447,8 +1465,8 @@ a uniform interface to call it from compiled code.}]; let NoCWrapper = true; let ReturnType = "JitCall"; let Args = [ - Arg<"TInterp_t", "I">, - Arg<"TCppConstFunction_t", "func"> + Arg<"InterpRef", "I">, + Arg<"ConstFuncRef", "func"> ]; } @@ -1481,7 +1499,7 @@ def UseExternalInterpreter : CppInterOpAPI { let Doc = [{Sets the Interpreter instance with an external interpreter, meant to be called before any other API calls.}]; let ReturnType = "void"; - let Args = [Arg<"TInterp_t", "I">]; + let Args = [Arg<"InterpRef", "I">]; } def MakeFunctionCallable : CppInterOpAPI { @@ -1490,7 +1508,76 @@ uniform interface to call it from compiled code.}]; // JitCall is a C++ class; no mechanical C mapping. let NoCWrapper = true; let ReturnType = "JitCall"; - let Args = [Arg<"TCppConstFunction_t", "func">]; + let Args = [Arg<"ConstFuncRef", "func">]; +} + +// --- Runtime vtable overlay: replace virtual slots on a live object --- + +def MakeVTableOverlay : CppInterOpAPI { + let Doc = [{Copy the vtable of \c inst, replace the slots of the listed +virtual \c methods with the given function pointers, and install the copy on +\c inst. The returned DRef owns the copy and remembers the original vptr; +DestroyVTableOverlay undoes the install. All non-overlaid slots and the +ABI prefix (offset-to-top + type_info on Itanium) are preserved, so the +destructor, RTTI and unrelated virtuals keep dispatching normally. Slot +indices and the vtable size are derived from \c base via Clang's +VTableContext, so the caller works in reflected methods, not ABI-specific +indices, and the result is correct for both Itanium and Microsoft ABIs. + +The install is per-instance: \c inst's vptr is the only pointer rewritten, +so other live or future instances of the same type continue to use the +class's original vtable. The rewrite covers only the primary vptr (offset +0 of \c inst); a class with multiple polymorphic direct bases or any +virtual base would have additional vptrs / vbase-offset entries the +overlay does not touch and is rejected outright -- see \returns. +\param[in] inst Object whose vptr is read and replaced. +\param[in] base Polymorphic class supplying \c inst's vtable layout. +\param[in] methods Virtual methods of \c base whose slots are replaced. +\param[in] overlay_fns Function pointers to install at those slots. +\param[in] n_overlays Length of \c methods and \c overlay_fns. +\param[in] n_extra_prefix_slots Optional \c void* slots prepended *before* the + ABI prefix in the owned block, initialized to nullptr. Bindings use + them to stash per-instance data adjacent to the vtable -- a thunk + reads its slot via \c VTableOverlayExtraSlot(self, i) with a single + fixed-offset load, avoiding a runtime registry lookup. +\param[in] on_destroy Optional callback invoked on the operator-delete path + (D0 on Itanium, the single deleting dtor on MSVC). Bindings use + it to release per-instance state tied to the object's lifetime + (Python handles, closures). Ordering is callback first, then the + original deleting destructor; the callback runs while \c inst is + still alive and may inspect its state. After the original dtor + returns, the object's memory has been freed -- the callback + cannot defer the read. The callback receives \c inst and + \c cleanup_data verbatim, runs inside a C++ destructor, and must + not throw -- an escaping exception is undefined behavior. The + callback also must not destroy this overlay (the original dtor + still has to run after it returns). Stack-only destruction (D1) + does not trigger the callback. Pass nullptr to skip the dtor + hook entirely (zero per-call overhead, slot not patched). +\param[in] cleanup_data Opaque pointer passed back to \c on_destroy. +\returns nullptr if \c inst is null, any method is null or not virtual, + \c base is not a polymorphic class, or \c base has multiple + polymorphic direct bases or any virtual base.}]; + let NoCWrapper = true; + let ReturnType = "VTableOverlay*"; + let Args = [ + Arg<"void*", "inst">, + Arg<"ConstDeclRef", "base">, + Arg<"const ConstFuncRef*", "methods">, + Arg<"void* const*", "overlay_fns">, + Arg<"std::size_t", "n_overlays">, + Arg<"std::size_t", "n_extra_prefix_slots", "0">, + Arg<"VTableOverlayDtorHook", "on_destroy", "nullptr">, + Arg<"void*", "cleanup_data", "nullptr"> + ]; +} + +def DestroyVTableOverlay : CppInterOpAPI { + let Doc = [{Restore the original vptr on the overlaid instance and free the +overlay. Null-safe.}]; + let NoCWrapper = true; + let ReturnType = "void"; + let Args = [Arg<"VTableOverlay*", "overlay">]; } def IsIntegerType : CppInterOpAPI { @@ -1498,7 +1585,7 @@ def IsIntegerType : CppInterOpAPI { If \p s is non-null, it is set to the signedness of the type.}]; let ReturnType = "bool"; let Args = [ - Arg<"TCppType_t", "type">, + Arg<"ConstTypeRef", "TyRef">, Arg<"Signedness*", "s", "nullptr"> ]; } @@ -1506,7 +1593,7 @@ If \p s is non-null, it is set to the signedness of the type.}]; def IsFloatingType : CppInterOpAPI { let Doc = "Checks if type has a floating representation."; let ReturnType = "bool"; - let Args = [Arg<"TCppType_t", "type">]; + let Args = [Arg<"ConstTypeRef", "TyRef">]; } def IsSameType : CppInterOpAPI { @@ -1514,15 +1601,15 @@ def IsSameType : CppInterOpAPI { i.e. have the same canonical type.}]; let ReturnType = "bool"; let Args = [ - Arg<"TCppType_t", "type_a">, - Arg<"TCppType_t", "type_b"> + Arg<"ConstTypeRef", "type_a">, + Arg<"ConstTypeRef", "type_b"> ]; } def IsVoidPointerType : CppInterOpAPI { let Doc = "Checks if type is a void pointer."; let ReturnType = "bool"; - let Args = [Arg<"TCppType_t", "type">]; + let Args = [Arg<"ConstTypeRef", "TyRef">]; } // Trace-hook impls invoked by JitCall's inline fast path through @@ -1533,7 +1620,7 @@ def CppInterOpTraceJitCallInvokeImpl : CppInterOpAPI { let Doc = "Trace hook for the JitCall::Invoke generic call path."; let ReturnType = "void"; let Args = [ - Arg<"const CppImpl::JitCall*", "JC">, + Arg<"const Cpp::JitCall*", "JC">, Arg<"void*", "result">, Arg<"void**", "args">, Arg<"std::size_t", "nargs">, @@ -1545,7 +1632,7 @@ def CppInterOpTraceJitCallInvokeDestructorImpl : CppInterOpAPI { let Doc = "Trace hook for the JitCall::InvokeDestructor path."; let ReturnType = "void"; let Args = [ - Arg<"const CppImpl::JitCall*", "JC">, + Arg<"const Cpp::JitCall*", "JC">, Arg<"void*", "object">, Arg<"unsigned long", "nary">, Arg<"int", "withFree"> @@ -1553,10 +1640,10 @@ def CppInterOpTraceJitCallInvokeDestructorImpl : CppInterOpAPI { } def CppInterOpTraceJitCallInvokeReturnImpl : CppInterOpAPI { - let Doc = "Trace hook for post-Invoke pointer-return handle registration."; + let Doc = "Trace hook for post-Invoke pointer-return DRef registration."; let ReturnType = "void"; let Args = [ - Arg<"const CppImpl::JitCall*", "JC">, + Arg<"const Cpp::JitCall*", "JC">, Arg<"void*", "result"> ]; } diff --git a/interpreter/CppInterOp/lib/CppInterOp/CppInterOpAPI.td b/interpreter/CppInterOp/lib/CppInterOp/CppInterOpAPI.td index b2ed594b6eea9..b5e6f56d73262 100644 --- a/interpreter/CppInterOp/lib/CppInterOp/CppInterOpAPI.td +++ b/interpreter/CppInterOp/lib/CppInterOp/CppInterOpAPI.td @@ -57,7 +57,7 @@ class TypedArg or ReturnType. +// TypeRef — the C++ type string as it appears in Arg<> or ReturnType. // CType — the C-compatible type to use in the generated wrapper. // ArgConversion — expression to convert a C argument to C++ before passing // to the Cpp:: function. Use $name as the placeholder for @@ -76,7 +76,7 @@ class TypedArg { - string CppType = cpp; + string TypeRef = cpp; string CType = c; string ArgConversion = argconv; string RetConversion = retconv; @@ -97,17 +97,20 @@ def CMap_int64_t : CTypeMap<"int64_t", "int64_t">; def CMap_uint64_t : CTypeMap<"uint64_t", "uint64_t">; def CMap_boolp : CTypeMap<"bool*", "bool*">; -// Opaque pointer types — erase to void* at the C boundary. -def CMap_TCppScope_t : CTypeMap<"TCppScope_t", "void*">; -def CMap_TCppConstScope_t : CTypeMap<"TCppConstScope_t", "const void*">; -def CMap_TCppType_t : CTypeMap<"TCppType_t", "void*">; -def CMap_TCppConstType_t : CTypeMap<"TCppConstType_t", "const void*">; -def CMap_TCppFunction_t : CTypeMap<"TCppFunction_t", "void*">; -def CMap_TCppConstFunc_t : CTypeMap<"TCppConstFunction_t", "const void*">; -def CMap_TCppFuncAddr_t : CTypeMap<"TCppFuncAddr_t", "void*">; -def CMap_TInterp_t : CTypeMap<"TInterp_t", "void*">; -def CMap_TCppObject_t : CTypeMap<"TCppObject_t", "void*">; -def CMap_TCppIndex_t : CTypeMap<"TCppIndex_t", "size_t">; +// Opaque handle types. C consumers see these as the prefixed global +// structs (CppDeclRef etc.) declared in CppInterOpTypes.h. C++ TUs that +// include CXCppInterOp.h get `using CppDeclRef = Cpp::DeclRef;` aliases +// from that header, so the same .inc declarations compile as either +// language. C++ TUs that include only the C++ API (CppInterOp.h) never +// see the CppDeclRef spelling at all. +def CMap_DeclRef : CTypeMap<"DeclRef", "CppDeclRef">; +def CMap_TypeRef : CTypeMap<"TypeRef", "CppTypeRef">; +def CMap_FuncRef : CTypeMap<"FuncRef", "CppFuncRef">; +def CMap_InterpRef : CTypeMap<"InterpRef", "CppInterpRef">; +def CMap_ObjectRef : CTypeMap<"ObjectRef", "CppObjectRef">; +def CMap_ConstDeclRef : CTypeMap<"ConstDeclRef", "CppConstDeclRef">; +def CMap_ConstTypeRef : CTypeMap<"ConstTypeRef", "CppConstTypeRef">; +def CMap_ConstFuncRef : CTypeMap<"ConstFuncRef", "CppConstFuncRef">; // String types — need conversion at the boundary. def CMap_string : CTypeMap<"std::string", "char*", @@ -118,7 +121,7 @@ def CMap_stringref : CTypeMap<"const std::string&", "const char*", // Enums — cast to/from the underlying integer type. // Conversions use Cpp:: qualification because the generated C wrappers -// live outside the CppImpl namespace. +// live outside the Cpp namespace. def CMap_QualKind : CTypeMap<"QualKind", "unsigned", /*ArgConv=*/"static_cast($name)", /*RetConv=*/"static_cast($result)">; @@ -152,15 +155,17 @@ def CMap_TemplateArgInfoPtr : CTypeMap<"const TemplateArgInfo*", //===----------------------------------------------------------------------===// // Vector return types → CppInterOpArray. -def CMap_VecScope_ret : CTypeMap<"std::vector", +def CMap_VecScope_ret : CTypeMap<"std::vector", "CppInterOpArray", "", "", "return", "void*">; -def CMap_VecFunc_ret : CTypeMap<"std::vector", +def CMap_VecFunc_ret : CTypeMap<"std::vector", "CppInterOpArray", "", "", "return", "void*">; // Vector out-param types → removed from C args, becomes CppInterOpArray return. -def CMap_VecScope_out : CTypeMap<"std::vector&", +def CMap_VecScope_out : CTypeMap<"std::vector&", "CppInterOpArray", "", "", "outparam", "void*">; -def CMap_VecFunc_out : CTypeMap<"std::vector&", +def CMap_VecFunc_out : CTypeMap<"std::vector&", + "CppInterOpArray", "", "", "outparam", "void*">; +def CMap_VecType_out : CTypeMap<"std::vector&", "CppInterOpArray", "", "", "outparam", "void*">; // String collection out-params → CppInterOpStringArray return. @@ -172,7 +177,7 @@ def CMap_SetStr_out : CTypeMap<"std::set&", // Vector in-param types → expanded to (ptr, size) in C args. def CMap_VecCharPP_in : CTypeMap<"const std::vector&", "const char**", "", "", "inparam", "const char*">; -def CMap_VecFunc_in : CTypeMap<"const std::vector&", +def CMap_VecFunc_in : CTypeMap<"const std::vector&", "void**", "", "", "inparam", "void*">; def CMap_VecTAI_in : CTypeMap<"const std::vector&", "const TemplateArgInfo*", "", "", "inparam", "TemplateArgInfo">; @@ -183,7 +188,7 @@ def CMap_VecTAI_in : CTypeMap<"const std::vector&", // Base class for all CppInterOp API functions. class CppInterOpAPI { - // The C++ function name as it appears in CppImpl::. + // The C++ function name as it appears in Cpp::. string CppName = NAME; // Return type. string ReturnType; diff --git a/interpreter/CppInterOp/lib/CppInterOp/CppInterOpDispatch.cpp b/interpreter/CppInterOp/lib/CppInterOp/CppInterOpDispatch.cpp index c02b5382cdbe8..176a168b0a8a9 100644 --- a/interpreter/CppInterOp/lib/CppInterOp/CppInterOpDispatch.cpp +++ b/interpreter/CppInterOp/lib/CppInterOp/CppInterOpDispatch.cpp @@ -1,7 +1,7 @@ // Implementation of the dispatch symbol table. Maps function names to -// CppImpl:: addresses for dlopen consumers via CppGetProcAddress. +// Cpp:: addresses for dlopen consumers via CppGetProcAddress. // This is a library internal — it includes CppInterOp.h (not Dispatch.h) -// because it needs the CppImpl:: function declarations. +// because it needs the Cpp:: function declarations. #include "CppInterOp/CppInterOp.h" @@ -9,7 +9,7 @@ #include #include -using namespace CppImpl; +using namespace Cpp; using CppFnPtrTy = void (*)(); // NOLINTBEGIN(cppcoreguidelines-pro-type-cstyle-cast) @@ -24,7 +24,7 @@ using CppFnPtrTy = void (*)(); #endif static const std::unordered_map DispatchMap = { #define CPPINTEROP_API_FUNC(DN, CN, Ret, DeclArgs, CallArgs, RawTypes) \ - {#DN, (CppFnPtrTy) static_cast(&CppImpl::CN)}, + {#DN, (CppFnPtrTy) static_cast(&Cpp::CN)}, #include "CppInterOp/CppInterOpAPI.inc" }; #if defined(__GNUC__) || defined(__clang__) diff --git a/interpreter/CppInterOp/lib/CppInterOp/ErrorInternal.cpp b/interpreter/CppInterOp/lib/CppInterOp/ErrorInternal.cpp new file mode 100644 index 0000000000000..62b1889835201 --- /dev/null +++ b/interpreter/CppInterOp/lib/CppInterOp/ErrorInternal.cpp @@ -0,0 +1,369 @@ +//===- ErrorInternal.cpp - Impl-side Result + ErrorRef + Diag --- C++ -===// +// +// Part of the compiler-research project, under the Apache License v2.0 with +// LLVM Exceptions. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "CppInterOp/Error.h" + +#include "CppInterOp/CppInterOpTypes.h" + +#include "Compatibility.h" +#include "ErrorInternal.h" +#include "InterpreterInfo.h" + +#include "clang/Basic/Diagnostic.h" +#include "clang/Basic/SourceLocation.h" +#include "clang/Basic/SourceManager.h" +#include "clang/Frontend/CompilerInstance.h" + +#include "llvm/ADT/SmallString.h" +#include "llvm/Support/raw_ostream.h" + +#include +#include +#include +#include + +namespace Cpp { + +// A null DiagnosticRef returns defaults so callers don't have to +// branch on isNull() before every access. +static const StoredDiagView* AsView(DiagnosticRef D) { + return static_cast(D.data); +} + +CPPINTEROP_API DiagnosticSeverity GetDiagnosticSeverity(DiagnosticRef D) { + if (const StoredDiagView* V = AsView(D)) + return V->Sev; + return DiagnosticSeverity::Note; +} + +CPPINTEROP_API const char* GetDiagnosticMessage(DiagnosticRef D) { + if (const StoredDiagView* V = AsView(D)) + return V->Message.c_str(); + return ""; +} + +CPPINTEROP_API const char* GetDiagnosticFile(DiagnosticRef D) { + if (const StoredDiagView* V = AsView(D)) + return V->File.c_str(); + return ""; +} + +CPPINTEROP_API unsigned GetDiagnosticLine(DiagnosticRef D) { + if (const StoredDiagView* V = AsView(D)) + return V->Line; + return 0; +} + +CPPINTEROP_API unsigned GetDiagnosticColumn(DiagnosticRef D) { + if (const StoredDiagView* V = AsView(D)) + return V->Column; + return 0; +} + +// Member-function forwarders. +DiagnosticSeverity DiagnosticRef::severity() const { + return GetDiagnosticSeverity(*this); +} +const char* DiagnosticRef::message() const { + return GetDiagnosticMessage(*this); +} +const char* DiagnosticRef::file() const { return GetDiagnosticFile(*this); } +unsigned DiagnosticRef::line() const { return GetDiagnosticLine(*this); } +unsigned DiagnosticRef::column() const { return GetDiagnosticColumn(*this); } + +CPPINTEROP_API Status GetStatus(ErrorRef E) { + if (E.isOk()) + return Status::Ok; + if (E.isInline()) + return E.inlineStatus(); + if (const ErrorSlice* S = E.slice()) + return S->Code; + return Status::Ok; +} + +CPPINTEROP_API ArrayView GetDiagnostics(ErrorRef E) { + const ErrorSlice* S = E.slice(); + if (!S || S->Diagnostics.empty()) + return {}; + // DiagnosticRefs are constructed lazily into a thread-local buffer + // so the returned ArrayView can alias contiguous + // storage (slice.Diagnostics is a std::deque -- not contiguous). + // The buffer survives until the next GetDiagnostics call on the + // same thread; callers that need to retain the view should copy it. + static thread_local std::vector Buf; + Buf.clear(); + Buf.reserve(S->Diagnostics.size()); + for (const StoredDiagView& Dv : S->Diagnostics) + Buf.push_back(DiagnosticRef{&Dv}); + return ArrayView{Buf.data(), Buf.size()}; +} + +// Member-function forwarders. +Status ErrorRef::status() const { return GetStatus(*this); } +ArrayView ErrorRef::diagnostics() const { + return GetDiagnostics(*this); +} +const char* ErrorRef::producer() const { + const ErrorSlice* S = slice(); + return S ? S->Producer : nullptr; +} +const char* ErrorRef::producerSignature() const { + const ErrorSlice* S = slice(); + return S ? S->ProducerSignature : nullptr; +} + +ErrorRecord ErrorRef::record() const { + ErrorRecord R; + R.Code = status(); + const ErrorSlice* S = slice(); + if (!S) + return R; + R.Diagnostics.reserve(S->Diagnostics.size()); + for (const StoredDiagView& Dv : S->Diagnostics) { + DiagnosticInfo Di; + Di.Severity = Dv.Sev; + Di.Message = Dv.Message; + Di.File = Dv.File; + Di.Line = Dv.Line; + Di.Column = Dv.Column; + R.Diagnostics.push_back(std::move(Di)); + } + R.Producer = S->Producer; + R.ProducerSignature = S->ProducerSignature; + return R; +} + +CPPINTEROP_API void RetainErrorRef(ErrorRef E) { + // Inline-encoded errors carry no refcount; the bits are pure value. + if (!E.isSlice()) + return; + E.slice()->Refcount.fetch_add(1, std::memory_order_acq_rel); +} + +CPPINTEROP_API void ReleaseErrorRef(ErrorRef E) { + if (!E.isSlice()) + return; + const ErrorSlice* S = E.slice(); + if (S->Refcount.fetch_sub(1, std::memory_order_acq_rel) == 1) + delete S; +} + +char StatusError::ID = 0; + +void StatusError::log(llvm::raw_ostream& OS) const { + OS << "CppInterOp status error: " << Message; +} +std::error_code StatusError::convertToErrorCode() const { + return llvm::inconvertibleErrorCode(); +} + +CPPINTEROP_API ErrorSlice* AllocSlice(InterpreterInfo* Owner) { + auto* S = new ErrorSlice(); + S->Owner = Owner; + return S; +} + +CPPINTEROP_API ErrorRef makeError(Status S) { return ErrorRef::makeInline(S); } + +CPPINTEROP_API ErrorRef makeError(InterpreterInfo* II, Status S, + std::string Message, const char* Producer, + const char* ProducerSig) { + ErrorSlice* Slc = AllocSlice(II); + Slc->Code = S; + Slc->Producer = Producer; + Slc->ProducerSignature = ProducerSig; + if (!Message.empty()) { + StoredDiagView Dv; + Dv.Message = std::move(Message); + Dv.Sev = DiagnosticSeverity::Error; + Slc->Diagnostics.push_back(std::move(Dv)); + } + DrainPendingInto(II, Slc); + // Refcount stays at the default 0; the wrapping Result bumps to 1 + // via its ErrorRef ctor. + return ErrorRef::makeSlice(Slc); +} + +CPPINTEROP_API ErrorRef drainError(InterpreterInfo* II, llvm::Error E, + const char* Producer, + const char* ProducerSig) { + // Decompose the llvm::Error chain into a single slice. StatusError + // sets the Status directly; any other ErrorInfo subclass is + // stringified via its log() and surfaced as Status::CompileError. + // Typed payloads (overload / deduction) and their llvm::Error + // subclasses arrive with the migrations that need them. + Status Code = Status::CompileError; + std::string Msg; + + llvm::handleAllErrors( + std::move(E), + [&](const StatusError& SE) { + Code = SE.Code; + if (Msg.empty()) + Msg = SE.Message; + }, + [&](const llvm::ErrorInfoBase& EIB) { + // Unknown subclass: stringify and surface as CompileError. + if (Msg.empty()) { + llvm::raw_string_ostream OS(Msg); + EIB.log(OS); + } + }); + + ErrorSlice* Slc = AllocSlice(II); + Slc->Code = Code; + Slc->Producer = Producer; + Slc->ProducerSignature = ProducerSig; + if (!Msg.empty()) { + StoredDiagView Dv; + Dv.Message = std::move(Msg); + Dv.Sev = DiagnosticSeverity::Error; + Slc->Diagnostics.push_back(std::move(Dv)); + } + DrainPendingInto(II, Slc); + return ErrorRef::makeSlice(Slc); +} + +namespace { +/// Captures every diagnostic the parser/sema emits into the owning +/// interpreter's StoredDiags and forwards to the previously installed +/// consumer (typically Clang's TextDiagnosticPrinter) so existing +/// stderr output is preserved. Owned holds the chained consumer when +/// the engine owned it (clang-REPL); Raw points at it regardless of +/// ownership so cling's externally-owned consumer also forwards. +class CppInteropDiagConsumer : public clang::DiagnosticConsumer { +public: + CppInteropDiagConsumer(InterpreterInfo* II, + std::unique_ptr Owned, + clang::DiagnosticConsumer* Raw) + : II(II), Owned(std::move(Owned)), Raw(Raw) {} + + void BeginSourceFile(const clang::LangOptions& LO, + const clang::Preprocessor* PP) override { + if (Raw) + Raw->BeginSourceFile(LO, PP); + } + void EndSourceFile() override { + if (Raw) + Raw->EndSourceFile(); + } + void HandleDiagnostic(clang::DiagnosticsEngine::Level Level, + const clang::Diagnostic& Info) override; + +private: + InterpreterInfo* II; + std::unique_ptr Owned; + clang::DiagnosticConsumer* Raw; +}; + +DiagnosticSeverity MapClangSeverity(clang::DiagnosticsEngine::Level L) { + switch (L) { + case clang::DiagnosticsEngine::Ignored: + case clang::DiagnosticsEngine::Note: + case clang::DiagnosticsEngine::Remark: + return DiagnosticSeverity::Note; + case clang::DiagnosticsEngine::Warning: + return DiagnosticSeverity::Warning; + case clang::DiagnosticsEngine::Error: + return DiagnosticSeverity::Error; + case clang::DiagnosticsEngine::Fatal: + return DiagnosticSeverity::Fatal; + } + return DiagnosticSeverity::Error; +} +} // namespace + +void CppInteropDiagConsumer::HandleDiagnostic( + clang::DiagnosticsEngine::Level Level, const clang::Diagnostic& Info) { + // Update the base consumer's NumErrors so hasErrorOccurred() keeps + // working for callers that still rely on it. + clang::DiagnosticConsumer::HandleDiagnostic(Level, Info); + if (Raw) + Raw->HandleDiagnostic(Level, Info); + + llvm::SmallString<128> Buf; + Info.FormatDiagnostic(Buf); + + StoredDiagView Dv; + Dv.Message = Buf.str().str(); + Dv.Sev = MapClangSeverity(Level); + + if (Info.hasSourceManager() && Info.getLocation().isValid()) { + clang::SourceManager& SM = Info.getSourceManager(); + clang::PresumedLoc PLoc = SM.getPresumedLoc(Info.getLocation()); + if (PLoc.isValid()) { + if (const char* F = PLoc.getFilename()) + Dv.File = F; + Dv.Line = PLoc.getLine(); + Dv.Column = PLoc.getColumn(); + } + } + + II->StoredDiags.push_back(std::move(Dv)); +} + +CPPINTEROP_API void InstallDiagConsumer(InterpreterInfo* II) { + // Chain to whatever consumer Clang already installed so existing + // stderr behaviour is preserved. takeClient transfers ownership when + // the engine owns it (clang-REPL); cling installs its consumer with + // ShouldOwnClient=false, so takeClient returns null there and we + // forward through the raw pointer instead. getClient still returns + // the original between takeClient and setClient. + clang::DiagnosticsEngine& Diag = II->Interpreter->getCI()->getDiagnostics(); + std::unique_ptr PrevOwned = Diag.takeClient(); + clang::DiagnosticConsumer* PrevRaw = Diag.getClient(); + auto* New = new CppInteropDiagConsumer(II, std::move(PrevOwned), PrevRaw); + Diag.setClient(New, /*ShouldOwnClient=*/true); +} + +CPPINTEROP_API void DrainPendingInto(InterpreterInfo* II, ErrorSlice* S) { + for (auto& Dv : II->StoredDiags) + S->Diagnostics.push_back(std::move(Dv)); + II->StoredDiags.clear(); +} + +CPPINTEROP_API void ClearPending(InterpreterInfo* II) { + II->StoredDiags.clear(); +} + +CPPINTEROP_API unsigned GetPendingDiagnosticCount(InterpRef I) { + return static_cast(GetInterpInfo(I)->StoredDiags.size()); +} + +CPPINTEROP_API DiagnosticRef GetPendingDiagnostic(unsigned Idx, InterpRef I) { + InterpreterInfo* II = GetInterpInfo(I); + if (Idx >= II->StoredDiags.size()) + return DiagnosticRef{}; + return DiagnosticRef{&II->StoredDiags[Idx]}; +} + +CPPINTEROP_API void ClearPendingDiagnostics(InterpRef I) { + GetInterpInfo(I)->StoredDiags.clear(); +} + +[[noreturn]] CPPINTEROP_API void +ResultAbort_ValueOnError(const ErrorRef& /*Err*/) { + std::fputs("Cpp::Result::value() called on an error-bearing " + "Result. Use value_or(fallback) for lenient semantics, " + "or branch on .ok() / .error() before calling .value().\n", + stderr); + std::abort(); +} + +#ifndef NDEBUG +[[noreturn]] CPPINTEROP_API void +ResultAbort_UncheckedOnDtor(const ErrorRef& /*Err*/) { + std::fputs("Cpp::Result destroyed without check (likely a dropped " + "error). Call .ok() / .error() / .value() to inspect, " + "or .ignore() to acknowledge.\n", + stderr); + std::abort(); +} +#endif + +} // namespace Cpp diff --git a/interpreter/CppInterOp/lib/CppInterOp/ErrorInternal.h b/interpreter/CppInterOp/lib/CppInterOp/ErrorInternal.h new file mode 100644 index 0000000000000..ad032de89e3af --- /dev/null +++ b/interpreter/CppInterOp/lib/CppInterOp/ErrorInternal.h @@ -0,0 +1,138 @@ +//===--- ErrorInternal.h - Impl-side error helpers --------------*- C++ -*-===// +// +// Part of the compiler-research project, under the Apache License v2.0 with +// LLVM Exceptions. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +// +// Impl-side counterpart to CppInterOp/Error.h: the ErrorSlice body, +// the llvm::Error subclasses, and the boundary translator. +// +//===----------------------------------------------------------------------===// + +#ifndef CPPINTEROP_LIB_ERRORINTERNAL_H +#define CPPINTEROP_LIB_ERRORINTERNAL_H + +#include "CppInterOp/CppInterOpTypes.h" +#include "CppInterOp/Error.h" + +#include "llvm/Support/Error.h" +#include "llvm/Support/raw_ostream.h" + +#include +#include +#include +#include +#include +#include + +namespace Cpp { + +struct InterpreterInfo; // defined in CppInterOp.cpp + +/// Owning record for one captured diagnostic. DiagnosticRef::data +/// points at one of these. +struct StoredDiagView { + std::string Message; + std::string File; + unsigned Line = 0; + unsigned Column = 0; + DiagnosticSeverity Sev = DiagnosticSeverity::Error; +}; + +/// Body of a slice-encoded error: drained diagnostics, producer +/// attribution, refcount. alignas(16) keeps the low bits of an +/// ErrorSlice* clear so ErrorRef's tag bit stays unambiguous. +struct alignas(16) ErrorSlice { + // Mutable so Retain/Release can act on a `const ErrorSlice*` taken + // off an ErrorRef accessor without round-tripping through const_cast. + mutable std::atomic Refcount{0}; + InterpreterInfo* Owner = nullptr; + Status Code = Status::Ok; + + // Stable element addresses under push_back so a DiagnosticRef can + // address an entry safely. + std::deque Diagnostics; + + // Pointer into static __func__ storage. No copy. + const char* Producer = nullptr; + // Pointer into static __PRETTY_FUNCTION__ storage. No copy. + const char* ProducerSignature = nullptr; +}; + +// +// Carry the structured information the boundary translator drains +// into a slice. Impl code returns llvm::Expected with these on +// the failure path; makeResult wraps them into Result. + +class CPPINTEROP_API StatusError : public llvm::ErrorInfo { +public: + static char ID; + Status Code; + std::string Message; + + StatusError(Status C, std::string M) : Code(C), Message(std::move(M)) {} + + void log(llvm::raw_ostream& OS) const override; + [[nodiscard]] std::error_code convertToErrorCode() const override; +}; + +/// Allocate a fresh slice on the heap, refcount 0. Caller bumps the +/// refcount on first use (the boundary translator does this when +/// wrapping the slice into a returned ErrorRef). +CPPINTEROP_API ErrorSlice* AllocSlice(InterpreterInfo* Owner); + +/// Build an inline-status ErrorRef (no diagnostics, no payload). +CPPINTEROP_API ErrorRef makeError(Status S); + +/// Build a slice-encoded error carrying a Clang-derived message + the +/// interp's pending diagnostics + producer attribution. +CPPINTEROP_API ErrorRef makeError(InterpreterInfo* II, Status S, + std::string Message, const char* Producer, + const char* ProducerSig); + +/// Drain `E` into a fresh slice, returning the slice via ErrorRef. +/// Handles StatusError; any other llvm::Error is stringified via +/// llvm::ErrorInfoBase::log into the slice's first diagnostic. +CPPINTEROP_API ErrorRef drainError(InterpreterInfo* II, llvm::Error E, + const char* Producer, + const char* ProducerSig); + +/// Convert llvm::Expected into Result. On the error path, drain +/// the carried llvm::Error into a slice. +template +Result makeResult(InterpreterInfo* II, llvm::Expected E, + const char* Producer, const char* ProducerSig) { + if (E) + return Result(std::move(*E)); + return Result(drainError(II, E.takeError(), Producer, ProducerSig)); +} + +inline Result makeResult(InterpreterInfo* II, llvm::Error E, + const char* Producer, const char* ProducerSig) { + if (!E) + return Result(); + return Result(drainError(II, std::move(E), Producer, ProducerSig)); +} + +/// Drain the consumer's per-interp buffer into a slice on failure. +/// Success paths just clear via ClearPending -- callers don't surface +/// Ok-path warnings today. +CPPINTEROP_API void DrainPendingInto(InterpreterInfo* II, ErrorSlice* S); +CPPINTEROP_API void ClearPending(InterpreterInfo* II); + +// +// Production callers read diagnostics off Result::error() once the +// boundary translator has drained them into a slice. These survive +// for tests of the consumer install path and as a peek for callers +// not yet migrated to Result-returning APIs. + +CPPINTEROP_API unsigned GetPendingDiagnosticCount(InterpRef I = nullptr); +CPPINTEROP_API DiagnosticRef GetPendingDiagnostic(unsigned Idx, + InterpRef I = nullptr); +CPPINTEROP_API void ClearPendingDiagnostics(InterpRef I = nullptr); + +} // namespace Cpp + +#endif // CPPINTEROP_LIB_ERRORINTERNAL_H diff --git a/interpreter/CppInterOp/lib/CppInterOp/InterpreterInfo.h b/interpreter/CppInterOp/lib/CppInterOp/InterpreterInfo.h new file mode 100644 index 0000000000000..6703a87ea0f32 --- /dev/null +++ b/interpreter/CppInterOp/lib/CppInterOp/InterpreterInfo.h @@ -0,0 +1,84 @@ +//===--- InterpreterInfo.h - Per-interpreter impl state ---------*- C++ -*-===// +// +// Part of the compiler-research project, under the Apache License v2.0 with +// LLVM Exceptions. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +// +// Impl-only header centralising the per-interpreter state struct used by +// CppInterOp.cpp and Error.cpp. Not part of the public API; never +// included from include/CppInterOp/. +// +//===----------------------------------------------------------------------===// + +#ifndef CPPINTEROP_LIB_INTERPRETERINFO_H +#define CPPINTEROP_LIB_INTERPRETERINFO_H + +#include "Compatibility.h" +#include "ErrorInternal.h" + +#include "clang/AST/Decl.h" +#include "clang/AST/Type.h" + +#include "llvm/ADT/StringMap.h" + +#include +#include + +namespace Cpp { + +struct InterpreterInfo { + compat::Interpreter* Interpreter = nullptr; + bool isOwned = true; + // Store the list of builtin types. + llvm::StringMap BuiltinMap; + // Per-interpreter wrapper caches. Keyed on AST nodes that belong to this + // interpreter, so the caches must be destroyed together with it. + std::map WrapperStore; + std::map DtorWrapperStore; + // A deque keeps element addresses stable so DiagnosticRef::data + // survives push_back. + std::deque StoredDiags; + + InterpreterInfo(compat::Interpreter* I, bool Owned) + : Interpreter(I), isOwned(Owned) {} + + InterpreterInfo(InterpreterInfo&& Other) noexcept + : Interpreter(Other.Interpreter), isOwned(Other.isOwned) { + Other.Interpreter = nullptr; + Other.isOwned = false; + } + InterpreterInfo& operator=(InterpreterInfo&& Other) noexcept { + if (this != &Other) { + if (isOwned) + delete Interpreter; + Interpreter = Other.Interpreter; + isOwned = Other.isOwned; + Other.Interpreter = nullptr; + Other.isOwned = false; + } + return *this; + } + + ~InterpreterInfo() { + if (isOwned) + delete Interpreter; + } + + InterpreterInfo(const InterpreterInfo&) = delete; + InterpreterInfo& operator=(const InterpreterInfo&) = delete; +}; + +/// Resolve an InterpRef to the impl-side struct. When I is null, +/// returns the active (last-created) interpreter. +CPPINTEROP_API InterpreterInfo* GetInterpInfo(InterpRef I = nullptr); + +/// Wire CppInterOp's DiagnosticConsumer into the interpreter's +/// DiagnosticsEngine so parser/sema diagnostics flow into II->StoredDiags +/// and continue forwarding to whatever consumer Clang already had. +CPPINTEROP_API void InstallDiagConsumer(InterpreterInfo* II); + +} // namespace Cpp + +#endif // CPPINTEROP_LIB_INTERPRETERINFO_H diff --git a/interpreter/CppInterOp/lib/CppInterOp/Tracing.cpp b/interpreter/CppInterOp/lib/CppInterOp/Tracing.cpp index ed23697f5c09d..0e4e0ebf5367d 100644 --- a/interpreter/CppInterOp/lib/CppInterOp/Tracing.cpp +++ b/interpreter/CppInterOp/lib/CppInterOp/Tracing.cpp @@ -29,12 +29,14 @@ // consumers carry their per-DSO copies, populated by LoadDispatchAPI. namespace CppInternal { namespace DispatchRaw { -CPPINTEROP_API void (*CppInterOpTraceJitCallInvokeImpl)( - const CppImpl::JitCall*, void*, void**, std::size_t, void*) = nullptr; +CPPINTEROP_API void (*CppInterOpTraceJitCallInvokeImpl)(const Cpp::JitCall*, + void*, void**, + std::size_t, + void*) = nullptr; CPPINTEROP_API void (*CppInterOpTraceJitCallInvokeDestructorImpl)( - const CppImpl::JitCall*, void*, unsigned long, int) = nullptr; + const Cpp::JitCall*, void*, unsigned long, int) = nullptr; CPPINTEROP_API void (*CppInterOpTraceJitCallInvokeReturnImpl)( - const CppImpl::JitCall*, void*) = nullptr; + const Cpp::JitCall*, void*) = nullptr; } // namespace DispatchRaw } // namespace CppInternal @@ -50,11 +52,11 @@ void InitTracing() { // Wire libclangCppInterOp's own slots; Dispatch.h consumers do // the equivalent via the X-macro in LoadDispatchAPI. ::CppInternal::DispatchRaw::CppInterOpTraceJitCallInvokeImpl = - &CppImpl::CppInterOpTraceJitCallInvokeImpl; + &Cpp::CppInterOpTraceJitCallInvokeImpl; ::CppInternal::DispatchRaw::CppInterOpTraceJitCallInvokeDestructorImpl = - &CppImpl::CppInterOpTraceJitCallInvokeDestructorImpl; + &Cpp::CppInterOpTraceJitCallInvokeDestructorImpl; ::CppInternal::DispatchRaw::CppInterOpTraceJitCallInvokeReturnImpl = - &CppImpl::CppInterOpTraceJitCallInvokeReturnImpl; + &Cpp::CppInterOpTraceJitCallInvokeReturnImpl; } std::optional TraceRegion::lookupOutMask(llvm::StringRef Name) { @@ -101,7 +103,7 @@ static void WriteVersionComment(llvm::raw_ostream& OS, static std::string GetCppInterOpLibPath() { #if defined(__linux__) || defined(__APPLE__) Dl_info info; - if (dladdr(reinterpret_cast(&CppImpl::GetVersion), &info) && + if (dladdr(reinterpret_cast(&Cpp::GetVersion), &info) && info.dli_fname) { llvm::StringRef Name(info.dli_fname); if (Name.contains(".so") || Name.ends_with(".dylib") || @@ -117,7 +119,7 @@ static void WriteBuildContext(llvm::raw_ostream& OS) { OS << "// Build context (CppInterOp configure-time snapshot):\n"; // Bind to a local: StringRef of the GetBuildInfo() rvalue would // dangle past the next `;`. - std::string Snapshot = CppImpl::GetBuildInfo(); + std::string Snapshot = Cpp::GetBuildInfo(); llvm::StringRef Info(Snapshot); while (!Info.empty()) { auto [Line, Rest] = Info.split('\n'); @@ -169,7 +171,7 @@ static void WriteReproducerPrologue(llvm::raw_ostream& OS, OS << "#ifdef CPPINTEROP_USE_DISPATCH\n"; OS << "#include \n\n"; // X-macro defines one DispatchRaw slot per public API entry. - OS << "using namespace CppImpl;\n"; + OS << "using namespace Cpp;\n"; OS << "#define CPPINTEROP_API_FUNC(DN, CN, Ret, DeclArgs, CallArgs, " "RawTypes) \\\n" " Ret(*CppInternal::DispatchRaw::DN) RawTypes = nullptr;\n" @@ -205,7 +207,7 @@ std::string TraceInfo::writeToFile(const std::string& Version) { llvm::raw_fd_ostream OS(FD, /*shouldClose=*/true); DumpScope Guard(*this); - std::string Ver = Version.empty() ? CppImpl::GetVersion() : Version; + std::string Ver = Version.empty() ? Cpp::GetVersion() : Version; WriteReproducerPrologue(OS, "crash reproducer", Ver, "Generated automatically — re-run to reproduce."); OS << "void reproducer() {\n"; @@ -257,7 +259,7 @@ void TraceInfo::StopRegion(const std::string& Version) { return; DumpScope Guard(*this); - std::string Ver = Version.empty() ? CppImpl::GetVersion() : Version; + std::string Ver = Version.empty() ? Cpp::GetVersion() : Version; WriteReproducerPrologue(OS, "trace region", Ver, "Generated automatically."); OS << "void reproducer() {\n"; OS << " initCppInterOpReproducer();\n"; diff --git a/interpreter/CppInterOp/lib/CppInterOp/Tracing.h b/interpreter/CppInterOp/lib/CppInterOp/Tracing.h index b17dd31d71fb9..5da7643066747 100644 --- a/interpreter/CppInterOp/lib/CppInterOp/Tracing.h +++ b/interpreter/CppInterOp/lib/CppInterOp/Tracing.h @@ -310,10 +310,17 @@ struct ReproBuffer { ReproBuffer() : OS(Buffer) {} - // Opaque handle pointers — resolved to their registered name. - // const void* subsumes void* via the standard add-const conversion, - // so one overload covers both TCppFunction_t (= void*) and - // TCppConstFunction_t (= const void*) public typedefs. + // Opaque handle structs — unwrap .data and resolve to registered name. + void append(Cpp::DeclRef h) { append(h.data); } + void append(Cpp::TypeRef h) { append(h.data); } + void append(Cpp::FuncRef h) { append(h.data); } + void append(Cpp::ObjectRef h) { append(h.data); } + void append(Cpp::InterpRef h) { append(h.data); } + void append(Cpp::ConstDeclRef h) { append(h.data); } + void append(Cpp::ConstTypeRef h) { append(h.data); } + void append(Cpp::ConstFuncRef h) { append(h.data); } + + // Raw void* pointers — resolved to their registered name. void append(const void* p) { if (!p) { OS << "nullptr"; @@ -380,12 +387,12 @@ struct ReproBuffer { OS << "}"; } - // TemplateArgInfo: brace-init through the (TCppScope_t, const char*) + // TemplateArgInfo: brace-init through the (DeclRef, const char*) // ctor so the reproducer compiles. m_Type takes the void* path // (renders as vN); nullptr m_IntegralValue must render as `nullptr` // (the ctor's default), not the empty string the const char* path // would produce. - void append(const CppImpl::TemplateArgInfo& tai) { + void append(const Cpp::TemplateArgInfo& tai) { OS << "Cpp::TemplateArgInfo{"; append(static_cast(tai.m_Type)); OS << ", "; @@ -484,6 +491,24 @@ struct is_pointer_vector> : std::true_type {}; template inline constexpr bool is_pointer_vector_v = is_pointer_vector::value; +/// Detect opaque handle structs (Cpp::DeclRef, Cpp::TypeRef, etc.) +template +inline constexpr bool is_handle_v = + std::is_same_v || std::is_same_v || + std::is_same_v || std::is_same_v || + std::is_same_v || std::is_same_v || + std::is_same_v || + std::is_same_v; + +/// Detect vector-of-handle types. +template struct is_handle_vector : std::false_type {}; +template <> +struct is_handle_vector> : std::true_type {}; +template <> +struct is_handle_vector> : std::true_type {}; +template +inline constexpr bool is_handle_vector_v = is_handle_vector::value; + /// Holds all the data that is only needed when tracing is active. /// Heap-allocated only when TheTraceInfo != nullptr, so disabled tracing /// pays zero cost beyond a single pointer + bool on the stack. @@ -665,12 +690,19 @@ class TraceRegion { if (!m_Data) return val; m_Data->Returned = true; - if constexpr (std::is_pointer_v) { + if constexpr (is_handle_v) { + m_Data->HasPtrResult = true; + m_Data->Result = const_cast(static_cast(val.data)); + } else if constexpr (std::is_pointer_v) { m_Data->HasPtrResult = true; // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast) m_Data->Result = const_cast(static_cast(val)); } else if constexpr (std::is_null_pointer_v) { m_Data->HasPtrResult = true; + } else if constexpr (is_handle_vector_v) { + m_Data->HasRetVec = true; + for (const auto& h : val) + m_Data->RetVecPtrs.push_back(h.data); } else if constexpr (is_pointer_vector_v) { // Snapshot the element pointers; the source vector is owned by // the API call site and goes out of scope before ~TraceRegion. diff --git a/interpreter/CppInterOp/lib/CppInterOp/Unwrap.h b/interpreter/CppInterOp/lib/CppInterOp/Unwrap.h new file mode 100644 index 0000000000000..9253dbdc1786b --- /dev/null +++ b/interpreter/CppInterOp/lib/CppInterOp/Unwrap.h @@ -0,0 +1,57 @@ +//===--- Unwrap.h - wrap/unwrap for opaque handle types ---------*- C++ -*-===// +// +// Part of the compiler-research project, under the Apache License v2.0 with +// LLVM Exceptions. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +// +// Internal helpers for converting between CppInterOp's typed opaque handles +// (DeclRef, TypeRef, FuncRef, ObjectRef, InterpRef and their const variants) +// and raw pointers to the underlying Clang AST nodes. Not part of the public +// API — included only from lib/CppInterOp/*.cpp and unit-test TUs that need +// to materialise typed pointers for testing the primitives themselves. +// +// Downstream consumers (cppyy, xeus-cpp) call the public C++ / C APIs and +// never need wrap/unwrap; this header is deliberately not reachable from +// include/CppInterOp/CppInterOp.h. +// +//===----------------------------------------------------------------------===// + +#ifndef CPPINTEROP_LIB_UNWRAP_H +#define CPPINTEROP_LIB_UNWRAP_H + +#include "CppInterOp/CppInterOpTypes.h" + +namespace Cpp { + +// wrap(ptr) materialises a handle from a raw pointer. +// unwrap(handle) extracts the raw pointer, statically cast to T*. +// Const handles yield const T* via their const void* data member. + +template Handle wrap(void* P) { return Handle{P}; } + +// Const handles from const pointers — no const_cast needed. +template Handle wrap(const void* P) { return Handle{P}; } + +// Mutable handles — unwrap to non-const pointer. +template T* unwrap(DeclRef H) { return static_cast(H.data); } +template T* unwrap(TypeRef H) { return static_cast(H.data); } +template T* unwrap(FuncRef H) { return static_cast(H.data); } +template T* unwrap(ObjectRef H) { return static_cast(H.data); } +template T* unwrap(InterpRef H) { return static_cast(H.data); } + +// Const handles — unwrap to const pointer. +template const T* unwrap(ConstDeclRef H) { + return static_cast(H.data); +} +template const T* unwrap(ConstTypeRef H) { + return static_cast(H.data); +} +template const T* unwrap(ConstFuncRef H) { + return static_cast(H.data); +} + +} // namespace Cpp + +#endif // CPPINTEROP_LIB_UNWRAP_H diff --git a/interpreter/CppInterOp/unittests/CMakeLists.txt b/interpreter/CppInterOp/unittests/CMakeLists.txt index 77ad000d5f151..0fcb403b0c5b7 100644 --- a/interpreter/CppInterOp/unittests/CMakeLists.txt +++ b/interpreter/CppInterOp/unittests/CMakeLists.txt @@ -7,6 +7,15 @@ if (NOT TARGET GTest::gtest AND NOT TARGET gtest) include(GoogleTest) endif() +# Google Benchmark backs the perf-as-validity tests. Fetch it the same way +# as gtest when the build doesn't already provide it. Standalone-only: when +# CppInterOp is embedded (e.g. in ROOT) we must not pull an extra +# ExternalProject into the host build. Skipped on WASM (unsupported there). +if (CPPINTEROP_BUILT_STANDALONE AND NOT EMSCRIPTEN + AND NOT TARGET benchmark::benchmark AND NOT TARGET benchmark) + include(GoogleBenchmark) +endif() + if(EMSCRIPTEN) if (TARGET GTest::gtest) # Target names in CMake >= v3.23 diff --git a/interpreter/CppInterOp/unittests/CppInterOp/CAPITest.cpp b/interpreter/CppInterOp/unittests/CppInterOp/CAPITest.cpp index 1da0adcda5e2a..71f709780238e 100644 --- a/interpreter/CppInterOp/unittests/CppInterOp/CAPITest.cpp +++ b/interpreter/CppInterOp/unittests/CppInterOp/CAPITest.cpp @@ -3,20 +3,13 @@ #include "Utils.h" +#include "CppInterOp/CXCppInterOp.h" + #include "gtest/gtest.h" #include #include -// Pull in the generated C declarations. -extern "C" { -#include "CppInterOp/CXCppInterOpDecl.inc" - -// Hand-written wrappers not in the .inc file. -CPPINTEROP_API Cpp::CppInterOpArray -cppinterop_GetClassTemplatedMethods(const char* name, void* parent); -} - using namespace TestUtils; TYPED_TEST(CppInterOpTest, CAPI_DeclareAndProcess) { @@ -29,21 +22,21 @@ TYPED_TEST(CppInterOpTest, CAPI_ScopeQueries) { TestFixture::CreateInterpreter(); Cpp::Declare("namespace CAPINs { class Foo {}; }"); - auto* global = cppinterop_GetGlobalScope(); + auto global = cppinterop_GetGlobalScope(); EXPECT_NE(global, nullptr); - auto* ns = cppinterop_GetScope("CAPINs", nullptr); + auto ns = cppinterop_GetScope("CAPINs", nullptr); EXPECT_NE(ns, nullptr); EXPECT_TRUE(cppinterop_IsNamespace(ns)); EXPECT_FALSE(cppinterop_IsClass(ns)); - auto* foo = cppinterop_GetScope("Foo", ns); + auto foo = cppinterop_GetScope("Foo", ns); EXPECT_NE(foo, nullptr); EXPECT_TRUE(cppinterop_IsClass(foo)); EXPECT_TRUE(cppinterop_IsComplete(foo)); EXPECT_GT(cppinterop_SizeOf(foo), 0U); - auto* parent = cppinterop_GetParentScope(foo); + auto parent = cppinterop_GetParentScope(foo); EXPECT_EQ(parent, ns); char* name = cppinterop_GetName(foo); @@ -55,11 +48,11 @@ TYPED_TEST(CppInterOpTest, CAPI_TypeQueries) { TestFixture::CreateInterpreter(); Cpp::Declare("class CAPIType { int x; };"); - auto* scope = cppinterop_GetNamed("CAPIType", nullptr); - auto* type = cppinterop_GetTypeFromScope(scope); + auto scope = cppinterop_GetNamed("CAPIType", nullptr); + auto type = cppinterop_GetTypeFromScope(scope); EXPECT_NE(type, nullptr); - auto* back = cppinterop_GetScopeFromType(type); + auto back = cppinterop_GetScopeFromType(type); EXPECT_EQ(back, scope); EXPECT_FALSE(cppinterop_IsPointerType(type)); @@ -75,17 +68,21 @@ TYPED_TEST(CppInterOpTest, CAPI_FunctionQueries) { TestFixture::CreateInterpreter(); Cpp::Declare("int capi_add(int a, int b) { return a + b; }"); - auto* fn = cppinterop_GetNamed("capi_add", nullptr); + auto fn = cppinterop_GetNamed("capi_add", nullptr); EXPECT_NE(fn, nullptr); EXPECT_TRUE(cppinterop_IsFunction(fn)); - EXPECT_FALSE(cppinterop_IsMethod(fn)); - EXPECT_EQ(cppinterop_GetFunctionNumArgs(fn), 2U); - EXPECT_EQ(cppinterop_GetFunctionRequiredArgs(fn), 2U); - - auto* ret = cppinterop_GetFunctionReturnType(fn); + // The C-API spelling (CppConstFuncRef) is reachable here via the alias + // in CXCppInterOp.h. Cross-kind narrowing is explicit via struct-init + // from .data, same as on the C++ side. + CppConstFuncRef fnAsFunc{fn.data}; + EXPECT_FALSE(cppinterop_IsMethod(fnAsFunc)); + EXPECT_EQ(cppinterop_GetFunctionNumArgs(fnAsFunc), 2U); + EXPECT_EQ(cppinterop_GetFunctionRequiredArgs(fnAsFunc), 2U); + + auto ret = cppinterop_GetFunctionReturnType(fnAsFunc); EXPECT_NE(ret, nullptr); - char* sig = cppinterop_GetFunctionSignature(fn); + char* sig = cppinterop_GetFunctionSignature(fnAsFunc); EXPECT_NE(sig, nullptr); EXPECT_TRUE(strstr(sig, "capi_add") != nullptr); free(sig); // NOLINT @@ -95,8 +92,8 @@ TYPED_TEST(CppInterOpTest, CAPI_VariableQueries) { TestFixture::CreateInterpreter(); Cpp::Declare("namespace CAPIVarNs { int gvar = 99; }"); - auto* ns = cppinterop_GetNamed("CAPIVarNs", nullptr); - auto* var = cppinterop_GetNamed("gvar", ns); + auto ns = cppinterop_GetNamed("CAPIVarNs", nullptr); + auto var = cppinterop_GetNamed("gvar", ns); EXPECT_NE(var, nullptr); EXPECT_TRUE(cppinterop_IsVariable(var)); } @@ -105,7 +102,7 @@ TYPED_TEST(CppInterOpTest, CAPI_EnumQueries) { TestFixture::CreateInterpreter(); Cpp::Declare("enum CAPIEnum { A, B, C };"); - auto* e = cppinterop_GetNamed("CAPIEnum", nullptr); + auto e = cppinterop_GetNamed("CAPIEnum", nullptr); EXPECT_TRUE(cppinterop_IsEnumScope(e)); } @@ -113,7 +110,7 @@ TYPED_TEST(CppInterOpTest, CAPI_TemplateQueries) { TestFixture::CreateInterpreter(); Cpp::Declare("template struct CAPITmpl { T val; };"); - auto* tmpl = cppinterop_GetNamed("CAPITmpl", nullptr); + auto tmpl = cppinterop_GetNamed("CAPITmpl", nullptr); EXPECT_TRUE(cppinterop_IsTemplate(tmpl)); EXPECT_FALSE(cppinterop_IsTemplateSpecialization(tmpl)); } @@ -130,11 +127,11 @@ TYPED_TEST(CppInterOpTest, CAPI_ClassMemberQueries) { }; )"); - auto* cls = cppinterop_GetNamed("CAPICls", nullptr); + auto cls = cppinterop_GetNamed("CAPICls", nullptr); EXPECT_TRUE(cppinterop_HasDefaultConstructor(cls)); EXPECT_TRUE(cppinterop_IsClassPolymorphic(cls)); - auto* dtor = cppinterop_GetDestructor(cls); + auto dtor = cppinterop_GetDestructor(cls); EXPECT_NE(dtor, nullptr); } @@ -148,8 +145,8 @@ TYPED_TEST(CppInterOpTest, CAPI_Construct) { TestFixture::CreateInterpreter(args); Cpp::Declare("struct CAPIObj { int x = 7; };"); - auto* scope = cppinterop_GetNamed("CAPIObj", nullptr); - auto* obj = cppinterop_Construct(scope, nullptr, 1); + auto scope = cppinterop_GetNamed("CAPIObj", nullptr); + auto obj = cppinterop_Construct(scope, nullptr, 1); EXPECT_NE(obj, nullptr); cppinterop_Destruct(obj, scope, true, 0); } @@ -160,8 +157,8 @@ TYPED_TEST(CppInterOpTest, CAPI_EnumFunctions) { TestFixture::CreateInterpreter(); Cpp::Declare("const int ci = 42;"); - auto* scope = cppinterop_GetNamed("ci", nullptr); - auto* type = cppinterop_GetVariableType(scope); + auto scope = cppinterop_GetNamed("ci", nullptr); + auto type = cppinterop_GetVariableType(scope); // HasTypeQualifier takes QualKind (unsigned in C). // QualKind::kConst == 1U @@ -169,26 +166,45 @@ TYPED_TEST(CppInterOpTest, CAPI_EnumFunctions) { EXPECT_FALSE(cppinterop_HasTypeQualifier(type, /*kVolatile=*/2U)); // RemoveTypeQualifier returns the unqualified type. - auto* unqual = cppinterop_RemoveTypeQualifier(type, /*kConst=*/1U); + auto unqual = cppinterop_RemoveTypeQualifier(type, /*kConst=*/1U); EXPECT_FALSE(cppinterop_HasTypeQualifier(unqual, /*kConst=*/1U)); // GetLanguage returns an InterpreterLanguage enum as unsigned char. // CXX == 5 (Unknown=0, Asm=1, CIR=2, LLVM_IR=3, C=4, CXX=5) EXPECT_EQ(cppinterop_GetLanguage(nullptr), 5); +} + +// Exercises the hand-written cppinterop_Evaluate C bridge. The C++ overload +// returning Cpp::Box has no C wrapper (NoCWrapper); C clients use this form. +TYPED_TEST(CppInterOpTest, CAPI_Evaluate) { + if (TypeParam::isOutOfProcess) + GTEST_SKIP() << "Evaluate not supported in OOP JIT"; + TestFixture::CreateInterpreter(); - // Evaluate with bool* HadError. - bool HadError = false; - EXPECT_EQ(cppinterop_Evaluate("42", &HadError), 42); - EXPECT_FALSE(HadError); - // Evaluate error path tested in Interpreter_DeclareSilent; not - // duplicated here to avoid LLVM 22 crash and Windows MSBuild issues. + bool had_error = true; // ensure success path clears it + EXPECT_EQ(cppinterop_Evaluate("42", &had_error), 42); + EXPECT_FALSE(had_error); + + // Null HadError pointer is documented as supported. + EXPECT_EQ(cppinterop_Evaluate("7 * 6", nullptr), 42); + + // Error path: function writes true to HadError and returns ~0UL. + // Use a no-Value-after-success snippet (pure declaration) instead of + // `#error`; both hit the `!V.hasValue()` arm in CXCppInterOp.cpp's + // legacy Evaluate, but `#error` emits clang diagnostics that MSBuild's + // error-regex flags as a compile failure on the Windows CI runner. + had_error = false; + EXPECT_EQ(static_cast( + cppinterop_Evaluate("class CAPIEvalNoValue {};", &had_error)), + ~0UL); + EXPECT_TRUE(had_error); } TYPED_TEST(CppInterOpTest, CAPI_CollectionReturn) { TestFixture::CreateInterpreter(); Cpp::Declare("enum CAPIColEnum { X, Y, Z };"); - auto* e = cppinterop_GetNamed("CAPIColEnum", nullptr); + auto e = cppinterop_GetNamed("CAPIColEnum", nullptr); Cpp::CppInterOpArray constants = cppinterop_GetEnumConstants(e); EXPECT_EQ(constants.size, 3U); EXPECT_NE(constants.data, nullptr); @@ -205,7 +221,7 @@ TYPED_TEST(CppInterOpTest, CAPI_CollectionOutParam) { }; )"); - auto* cls = cppinterop_GetNamed("CAPIMembers", nullptr); + auto cls = cppinterop_GetNamed("CAPIMembers", nullptr); Cpp::CppInterOpArray members = cppinterop_GetDatamembers(cls); EXPECT_EQ(members.size, 2U); // a and b (not static c) cppinterop_DisposeArray(members); @@ -219,7 +235,7 @@ TYPED_TEST(CppInterOpTest, CAPI_StringCollectionOutParam) { TestFixture::CreateInterpreter(); Cpp::Declare("namespace CAPIStrNs { enum E1 { P, Q }; enum E2 { R }; }"); - auto* ns = cppinterop_GetNamed("CAPIStrNs", nullptr); + auto ns = cppinterop_GetNamed("CAPIStrNs", nullptr); Cpp::CppInterOpStringArray enums = cppinterop_GetEnums(ns); EXPECT_EQ(enums.size, 2U); // Verify the enum names are present (order may vary). @@ -242,7 +258,7 @@ TYPED_TEST(CppInterOpTest, CAPI_StringCollectionOutParam) { TYPED_TEST(CppInterOpTest, CAPI_CreateInterpreterInParam) { // Test the vector in-param pattern: CreateInterpreter(ptr, size, ptr, size). const char* args[] = {"-std=c++17"}; - auto* I = cppinterop_CreateInterpreter(args, 1, nullptr, 0); + auto I = cppinterop_CreateInterpreter(args, 1, nullptr, 0); EXPECT_NE(I, nullptr); } @@ -256,7 +272,7 @@ TYPED_TEST(CppInterOpTest, CAPI_GetClassTemplatedMethods) { }; )"); - auto* cls = cppinterop_GetNamed("CAPITmplMethods", nullptr); + auto cls = cppinterop_GetNamed("CAPITmplMethods", nullptr); ASSERT_NE(cls, nullptr); // "get" has templated methods. @@ -297,3 +313,28 @@ TYPED_TEST(CppInterOpTest, CAPI_StringFunctions) { free(demangled); // NOLINT #endif } + +// --- Handle tagging death tests --- +// Verify that passing the wrong kind of handle (e.g. a Type where a +// Scope is expected) triggers an assertion. These only fire in debug +// builds with assertions enabled. + +TYPED_TEST(CppInterOpTest, CAPI_OpaqueHandleTypes) { + TestFixture::CreateInterpreter(); + Cpp::Declare("class HandleTest { int x; };"); + + // wrap/unwrap round-trip through the C++ API. + auto raw = Cpp::GetNamed("HandleTest"); + Cpp::DeclRef decl = Cpp::wrap(raw.data); + EXPECT_EQ(Cpp::unwrap(decl), raw.data); + + auto rawType = Cpp::GetTypeFromScope(raw); + Cpp::TypeRef type = Cpp::wrap(rawType.data); + EXPECT_EQ(Cpp::unwrap(type), rawType.data); + + // DeclRef and TypeRef are distinct types: + // Cpp::unwrap(type) compiles — correct kind + // Cpp::unwrap(decl) compiles — correct kind + // But passing TypeRef where DeclRef is expected would not compile + // once the API signatures use the typed handles. +} diff --git a/interpreter/CppInterOp/unittests/CppInterOp/CMakeLists.txt b/interpreter/CppInterOp/unittests/CppInterOp/CMakeLists.txt index 09703a09fe6c7..adb2acadebad5 100644 --- a/interpreter/CppInterOp/unittests/CppInterOp/CMakeLists.txt +++ b/interpreter/CppInterOp/unittests/CppInterOp/CMakeLists.txt @@ -45,18 +45,25 @@ endif() add_cppinterop_unittest(CppInterOpTests CAPITest.cpp CommentReflectionTest.cpp + CppInterOpThunksTest.cpp + DiagnosticTest.cpp EnumReflectionTest.cpp FunctionReflectionTest.cpp + HandleTypesTest.cpp InterpreterTest.cpp JitTest.cpp + ResultTest.cpp ScopeReflectionTest.cpp TypeReflectionTest.cpp Utils.cpp VariableReflectionTest.cpp ) if (NOT EMSCRIPTEN) - # Emscripten has no GPU; CUDA tests are native-only. - target_sources(CppInterOpTests PRIVATE CUDATest.cpp) + # Emscripten has no GPU; CUDA tests are native-only. The VTableOverlay + # tests need Cpp::Construct, whose placement-new JIT wrapper does not + # compile under the Emscripten clang-repl (see FunctionReflection_Construct, + # likewise skipped there). + target_sources(CppInterOpTests PRIVATE CUDATest.cpp VTableOverlayTest.cpp) endif() if(NOT WIN32) @@ -117,6 +124,19 @@ endif() add_subdirectory(TestSharedLib) add_dependencies(DynamicLibraryManagerTests TestSharedLib) +# Cross-TU VTableOverlay perf check, built as part of the test suite. +# Standalone-only (its Google Benchmark dependency is provisioned only then; +# keeps it out of embedded host builds such as ROOT). Excluded on WASM +# (Google Benchmark's WASM support is shaky, and the dispatch-cost claim is +# about CPU, not JS, execution). The dispatch loop is compiled into +# TestSharedLib so the call site cannot be devirtualized in this TU. +if(CPPINTEROP_BUILT_STANDALONE AND NOT EMSCRIPTEN AND BUILD_SHARED_LIBS) + add_cppinterop_unittest(VTableOverlayCrossTUBench + VTableOverlayCrossTUBench.cpp) + target_link_libraries(VTableOverlayCrossTUBench PRIVATE benchmark TestSharedLib) + add_dependencies(VTableOverlayCrossTUBench TestSharedLib) +endif() + # Downstream-DSO dlopen check that mirrors libcppyy-backend's contract: # a SHARED lib using without linking against # clangCppInterOp, dlopen'd by a standalone exec that also doesn't link. diff --git a/interpreter/CppInterOp/unittests/CppInterOp/CUDATest.cpp b/interpreter/CppInterOp/unittests/CppInterOp/CUDATest.cpp index 0a1d5554c57bd..721e469eab57e 100644 --- a/interpreter/CppInterOp/unittests/CppInterOp/CUDATest.cpp +++ b/interpreter/CppInterOp/unittests/CppInterOp/CUDATest.cpp @@ -22,7 +22,7 @@ class CUDATest : public ::testing::Test { #if defined(CPPINTEROP_USE_CLING) || defined(_WIN32) return; // FIXME: Enable for cling and Windows. #endif - auto *I = Cpp::CreateInterpreter({}, {"--cuda"}); + auto I = Cpp::CreateInterpreter({}, {"--cuda"}); if (!I) return; if (Cpp::Declare("__global__ void test_func() {}" @@ -35,8 +35,9 @@ class CUDATest : public ::testing::Test { Cpp::DeleteInterpreter(I); return; } - bool evalErr = false; - int cudaErr = (int)Cpp::Evaluate("(int)cudaGetLastError()", &evalErr); + Cpp::Box v = Cpp::Evaluate("(int)cudaGetLastError()"); + bool evalErr = v.getKind() == Cpp::Box::K_Unspecified; + int cudaErr = evalErr ? -1 : v.unbox(); if (evalErr || cudaErr != 0) GTEST_LOG_(WARNING) << "CUDA Kernel execution failed (cudaError=" << cudaErr << "). Runtime tests will be skipped."; @@ -113,10 +114,9 @@ TEST_F(CUDATest, CUDARuntime) { int deviceCount = 0; cudaGetDeviceCount(&deviceCount); )")); - bool err = false; - intptr_t count = Cpp::Evaluate("deviceCount", &err); - EXPECT_FALSE(err); - EXPECT_GT((int)count, 0); + Cpp::Box count = Cpp::Evaluate("deviceCount"); + EXPECT_NE(count.getKind(), Cpp::Box::K_Unspecified); + EXPECT_GT(count.unbox(), 0); } TEST_F(CUDATest, Interpreter_GetLanguageCUDA) { @@ -140,15 +140,15 @@ TEST_F(CUDATest, SimpleKernelExecution) { int kernelErr = (int)cudaGetLastError(); )")) << "Failed to declare/launch kernel"; - bool err = false; - int cudaErr = (int)Cpp::Evaluate("kernelErr", &err); - EXPECT_FALSE(err); + Cpp::Box kernelErrV = Cpp::Evaluate("kernelErr"); + EXPECT_NE(kernelErrV.getKind(), Cpp::Box::K_Unspecified); + int cudaErr = kernelErrV.unbox(); EXPECT_EQ(cudaErr, 0) << "Simple kernel launch failed"; if (cudaErr == 0) { - intptr_t result = Cpp::Evaluate("*d", &err); - EXPECT_FALSE(err); - EXPECT_EQ((int)result, 42); + Cpp::Box result = Cpp::Evaluate("*d"); + EXPECT_NE(result.getKind(), Cpp::Box::K_Unspecified); + EXPECT_EQ(result.unbox(), 42); } Cpp::Declare("cudaFree(d);"); @@ -195,16 +195,13 @@ TEST_F(CUDATest, CUB_BlockReduceWithDeviceFunction) { cudaError_t lastErr = cudaGetLastError(); )")) << "Failed to launch kernel"; - bool err = false; - EXPECT_EQ((int)Cpp::Evaluate("(int)syncErr", &err), 0); - EXPECT_FALSE(err); - EXPECT_EQ((int)Cpp::Evaluate("(int)lastErr", &err), 0); - EXPECT_FALSE(err); + EXPECT_EQ(Cpp::Evaluate("(int)syncErr").unbox(), 0); + EXPECT_EQ(Cpp::Evaluate("(int)lastErr").unbox(), 0); // read result directly from managed memory - intptr_t result = Cpp::Evaluate("*d_out", &err); - EXPECT_FALSE(err); - EXPECT_EQ((int)result, 5625216); + Cpp::Box result = Cpp::Evaluate("*d_out"); + EXPECT_NE(result.getKind(), Cpp::Box::K_Unspecified); + EXPECT_EQ(result.unbox(), 5625216); Cpp::Declare("cudaFree(d_in); cudaFree(d_out);"); } diff --git a/interpreter/CppInterOp/unittests/CppInterOp/CppInterOpThunksTest.cpp b/interpreter/CppInterOp/unittests/CppInterOp/CppInterOpThunksTest.cpp new file mode 100644 index 0000000000000..8fde1bc7e571b --- /dev/null +++ b/interpreter/CppInterOp/unittests/CppInterOp/CppInterOpThunksTest.cpp @@ -0,0 +1,94 @@ +#include "CppInterOp/CppInterOpThunks.h" + +#include "gtest/gtest.h" + +#include + +// Cpp::Thunks::dispatch turns a per-call +// virtual dispatch into the binding's Traits::Call. It is consumed in +// two ways: (a) directly as a function-pointer constant, e.g. +// void* slot = &Cpp::Thunks::dispatch; +// to install into an overlay's vtable slot; (b) called like an ordinary +// function for unit testing. This file exercises (b) end-to-end and +// verifies the Traits contract. + +namespace { + +// Tiny stand-in for the per-instance binding state. A real binding +// (CPyCppyy's PyVirtualHandler) keeps a richer object here. +struct Handler { + int Value; +}; + +// Traits contract: From() locates the binding's Handler from `self`, +// Call() marshals and forwards. The template form matches what +// dispatch instantiates against. +struct AccumTraits { + using HandlerType = Handler; + static Handler& From(void* self) { return *static_cast(self); } + template + static R Call(Handler& H, std::size_t Slot, Args... args) { + // Mimic a marshaling kernel: fold all inputs into the return value + // so each (R, Slot, Args...) instantiation produces a distinguishable + // result the test can pin. Accumulate in R so the test can exercise + // non-integral returns without truncation. + R acc = static_cast(H.Value) + static_cast(Slot); + ((acc += static_cast(args)), ...); + return acc; + } +}; + +// Void-return / no-Args Traits. Lives at namespace scope because C++17 +// forbids member templates inside local classes (which is what +// TEST(...) bodies introduce). +struct VoidTraits { + using HandlerType = int; + static int& From(void* self) { return *static_cast(self); } + template static R Call(int& H, std::size_t Slot) { + H += static_cast(Slot) + 1; + } +}; + +} // namespace + +// dispatch is a function template. Each (Traits, Slot, R, Args...) tuple +// is a distinct instantiation with the right calling convention to be +// installed at a virtual-method slot. +TEST(CppInterOpThunks, DispatchForwardsThroughTraits) { + Handler H{100}; + + // (Slot=0, R=int, Args=int) + int (*Fn0)(void*, int) = &Cpp::Thunks::dispatch; + EXPECT_EQ(Fn0(&H, 7), 107); // 100 + 0 + 7 + + // (Slot=3, R=long long, Args=int, int) -- exercises a wider signature + long long (*Fn1)(void*, int, int) = + &Cpp::Thunks::dispatch; + EXPECT_EQ(Fn1(&H, 4, 5), 112); // 100 + 3 + 4 + 5 + + // (Slot=2, R=double, Args=double) -- non-integral return + double (*Fn2)(void*, double) = + &Cpp::Thunks::dispatch; + EXPECT_DOUBLE_EQ(Fn2(&H, 0.5), 102.5); // 100 + 2 + 0.5 +} + +// Different (Slot, R, Args...) tuples must yield different function +// pointers; the dispatcher is not folded across slots even when the +// underlying Call is the same template. +TEST(CppInterOpThunks, DistinctInstantiationsHaveDistinctAddresses) { + void* A = + reinterpret_cast(&Cpp::Thunks::dispatch); + void* B = + reinterpret_cast(&Cpp::Thunks::dispatch); + EXPECT_NE(A, B); // Slot 0 vs Slot 1 +} + +// Zero-arg case: dispatch with no Args... and a void return mirrors the +// signature of a virtual method `void foo()` -- the minimal shape an +// overlay would install. +TEST(CppInterOpThunks, DispatchWithNoArgsAndVoidReturn) { + int H = 0; + void (*Fn)(void*) = &Cpp::Thunks::dispatch; + Fn(&H); + EXPECT_EQ(H, 6); // 0 + 5 + 1 +} diff --git a/interpreter/CppInterOp/unittests/CppInterOp/DiagnosticTest.cpp b/interpreter/CppInterOp/unittests/CppInterOp/DiagnosticTest.cpp new file mode 100644 index 0000000000000..8015454369ee5 --- /dev/null +++ b/interpreter/CppInterOp/unittests/CppInterOp/DiagnosticTest.cpp @@ -0,0 +1,157 @@ +//===- DiagnosticTest.cpp - Tests for Cpp::DiagnosticRef -------*- C++ -*-===// +// +// Part of the compiler-research project, under the Apache License v2.0 with +// LLVM Exceptions. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "CppInterOp/CppInterOp.h" +#include "CppInterOp/Error.h" + +#include "../../lib/CppInterOp/ErrorInternal.h" + +#include "Utils.h" + +#include "gtest/gtest.h" + +#include + +// Size invariants. Refactors that inflate the public handle or the +// severity enum fail the build. +static_assert(sizeof(Cpp::DiagnosticRef) == sizeof(void*), + "DiagnosticRef must be a single pointer"); +static_assert(sizeof(Cpp::DiagnosticSeverity) == 1, + "DiagnosticSeverity must stay one byte"); + +TEST(DiagnosticTest, AccessorsRoundTripStoredView) { + Cpp::StoredDiagView V; + V.Message = "expected ';' after expression"; + V.File = "/tmp/foo.cpp"; + V.Line = 12; + V.Column = 5; + V.Sev = Cpp::DiagnosticSeverity::Error; + + Cpp::DiagnosticRef D{&V}; + EXPECT_FALSE(D.isNull()); + EXPECT_EQ(Cpp::GetDiagnosticSeverity(D), Cpp::DiagnosticSeverity::Error); + EXPECT_STREQ(Cpp::GetDiagnosticMessage(D), "expected ';' after expression"); + EXPECT_STREQ(Cpp::GetDiagnosticFile(D), "/tmp/foo.cpp"); + EXPECT_EQ(Cpp::GetDiagnosticLine(D), 12U); + EXPECT_EQ(Cpp::GetDiagnosticColumn(D), 5U); +} + +TEST(DiagnosticTest, SeverityRoundTrip) { + const Cpp::DiagnosticSeverity AllSev[] = { + Cpp::DiagnosticSeverity::Note, + Cpp::DiagnosticSeverity::Warning, + Cpp::DiagnosticSeverity::Error, + Cpp::DiagnosticSeverity::Fatal, + }; + for (Cpp::DiagnosticSeverity S : AllSev) { + Cpp::StoredDiagView V; + V.Sev = S; + Cpp::DiagnosticRef D{&V}; + EXPECT_EQ(Cpp::GetDiagnosticSeverity(D), S); + } +} + +TEST(DiagnosticTest, NullRefReturnsDefaults) { + Cpp::DiagnosticRef D; + EXPECT_TRUE(D.isNull()); + EXPECT_EQ(Cpp::GetDiagnosticSeverity(D), Cpp::DiagnosticSeverity::Note); + EXPECT_STREQ(Cpp::GetDiagnosticMessage(D), ""); + EXPECT_STREQ(Cpp::GetDiagnosticFile(D), ""); + EXPECT_EQ(Cpp::GetDiagnosticLine(D), 0U); + EXPECT_EQ(Cpp::GetDiagnosticColumn(D), 0U); +} + +// Empty strings must surface as "" not nullptr so callers can pass +// the result to strlen / strcmp without a separate null guard. +TEST(DiagnosticTest, EmptyStringsReturnEmptyCStrings) { + Cpp::StoredDiagView V; + Cpp::DiagnosticRef D{&V}; + ASSERT_NE(Cpp::GetDiagnosticMessage(D), nullptr); + ASSERT_NE(Cpp::GetDiagnosticFile(D), nullptr); + EXPECT_STREQ(Cpp::GetDiagnosticMessage(D), ""); + EXPECT_STREQ(Cpp::GetDiagnosticFile(D), ""); +} + +TEST(DiagnosticTest, TwoIndependentDiagsKeepTheirFields) { + Cpp::StoredDiagView A; + A.Message = "first"; + A.Line = 1; + Cpp::StoredDiagView B; + B.Message = "second"; + B.Line = 99; + + Cpp::DiagnosticRef DA{&A}; + Cpp::DiagnosticRef DB{&B}; + EXPECT_STREQ(Cpp::GetDiagnosticMessage(DA), "first"); + EXPECT_EQ(Cpp::GetDiagnosticLine(DA), 1U); + EXPECT_STREQ(Cpp::GetDiagnosticMessage(DB), "second"); + EXPECT_EQ(Cpp::GetDiagnosticLine(DB), 99U); +} + +// Out-of-range index must yield a null DiagnosticRef rather than +// indexing past the end of the deque. +TYPED_TEST(CPPINTEROP_TEST_MODE, GetPendingDiagnostic_OutOfRangeIsNull) { + TestFixture::CreateInterpreter(); + Cpp::ClearPendingDiagnostics(); + EXPECT_TRUE(Cpp::GetPendingDiagnostic(0).isNull()); + EXPECT_TRUE(Cpp::GetPendingDiagnostic(999).isNull()); +} + +// End-to-end: the diagnostic consumer wired into every interpreter +// must capture a real Clang parse error and surface it through the +// pending-diagnostic accessors. +TYPED_TEST(CPPINTEROP_TEST_MODE, Consumer_CapturesParseError) { +#ifdef _WIN32 + // The chained TextDiagnosticPrinter emits "error: expected expression" + // to stderr, which MSBuild's check-cppinterop custom-build rule scans + // and treats as a build failure regardless of the gtest result. + GTEST_SKIP() << "chained diagnostic trips MSBuild error scanning on Windows"; +#endif + TestFixture::CreateInterpreter(); + Cpp::ClearPendingDiagnostics(); + ASSERT_EQ(Cpp::GetPendingDiagnosticCount(), 0U); + + // Trigger a parse error. silent=false lets the diagnostic reach the + // consumer; silent=true sets SuppressAllDiagnostics, which short- + // circuits before the consumer chain. + EXPECT_NE(0, Cpp::Declare("int err = ;", /*silent=*/false)); + + ASSERT_GT(Cpp::GetPendingDiagnosticCount(), 0U); + Cpp::DiagnosticRef D = Cpp::GetPendingDiagnostic(0); + EXPECT_FALSE(D.isNull()); + EXPECT_EQ(Cpp::GetDiagnosticSeverity(D), Cpp::DiagnosticSeverity::Error); + EXPECT_GT(std::strlen(Cpp::GetDiagnosticMessage(D)), 0U); + + Cpp::ClearPendingDiagnostics(); + EXPECT_EQ(Cpp::GetPendingDiagnosticCount(), 0U); +} + +// Successful Declare must not leave stale diagnostics behind. +TYPED_TEST(CPPINTEROP_TEST_MODE, Consumer_OkDeclareLeavesBufferEmpty) { + TestFixture::CreateInterpreter(); + Cpp::ClearPendingDiagnostics(); + EXPECT_EQ(0, Cpp::Declare("int ok = 7;", /*silent=*/false)); + // Warnings might accrue, but no error-level diagnostics should. + for (unsigned I = 0, N = Cpp::GetPendingDiagnosticCount(); I < N; ++I) { + Cpp::DiagnosticRef D = Cpp::GetPendingDiagnostic(I); + EXPECT_NE(Cpp::GetDiagnosticSeverity(D), Cpp::DiagnosticSeverity::Error); + EXPECT_NE(Cpp::GetDiagnosticSeverity(D), Cpp::DiagnosticSeverity::Fatal); + } + Cpp::ClearPendingDiagnostics(); +} + +// silent=true sets SuppressAllDiagnostics on the engine, which +// short-circuits before any consumer in the chain runs. Pin this +// contract: a failed declare under silent=true reports failure via +// the return code but the consumer captures nothing. +TYPED_TEST(CPPINTEROP_TEST_MODE, Consumer_SilentSuppressesCapture) { + TestFixture::CreateInterpreter(); + Cpp::ClearPendingDiagnostics(); + EXPECT_NE(0, Cpp::Declare("int err = ;", /*silent=*/true)); + EXPECT_EQ(Cpp::GetPendingDiagnosticCount(), 0U); +} diff --git a/interpreter/CppInterOp/unittests/CppInterOp/DispatchInit.cpp b/interpreter/CppInterOp/unittests/CppInterOp/DispatchInit.cpp index c47ccd8f1f8da..7a5c5723b9734 100644 --- a/interpreter/CppInterOp/unittests/CppInterOp/DispatchInit.cpp +++ b/interpreter/CppInterOp/unittests/CppInterOp/DispatchInit.cpp @@ -4,7 +4,7 @@ #include "CppInterOp/Dispatch.h" // Define storage for all raw dispatch function pointers. -using namespace CppImpl; +using namespace Cpp; #define CPPINTEROP_API_FUNC(DN, CN, Ret, DeclArgs, CallArgs, RawTypes) \ Ret(*CppInternal::DispatchRaw::DN) RawTypes = nullptr; #include "CppInterOp/CppInterOpAPI.inc" diff --git a/interpreter/CppInterOp/unittests/CppInterOp/DispatchSmokeTest.cpp b/interpreter/CppInterOp/unittests/CppInterOp/DispatchSmokeTest.cpp index 63924449c64df..11d4ae0b0b8ee 100644 --- a/interpreter/CppInterOp/unittests/CppInterOp/DispatchSmokeTest.cpp +++ b/interpreter/CppInterOp/unittests/CppInterOp/DispatchSmokeTest.cpp @@ -15,8 +15,8 @@ // --- Interpreter --- TEST(DispatchSmokeTest, CreateAndDeleteInterpreter) { - auto* I = Cpp::CreateInterpreter({}); - EXPECT_NE(I, nullptr); + auto I = Cpp::CreateInterpreter({}); + EXPECT_TRUE(I); EXPECT_TRUE(Cpp::DeleteInterpreter()); } @@ -32,15 +32,15 @@ TEST(DispatchSmokeTest, ScopeLookup) { Cpp::CreateInterpreter({}); Cpp::Declare("namespace DispNS { class Foo {}; }"); - auto* global = Cpp::GetGlobalScope(); - EXPECT_NE(global, nullptr); + auto global = Cpp::GetGlobalScope(); + EXPECT_TRUE(global); - auto* ns = Cpp::GetNamed("DispNS"); - EXPECT_NE(ns, nullptr); + auto ns = Cpp::GetNamed("DispNS"); + EXPECT_TRUE(ns); EXPECT_TRUE(Cpp::IsNamespace(ns)); - auto* foo = Cpp::GetScope("Foo", ns); - EXPECT_NE(foo, nullptr); + auto foo = Cpp::GetScope("Foo", ns); + EXPECT_TRUE(foo); EXPECT_TRUE(Cpp::IsClass(foo)); EXPECT_FALSE(Cpp::IsNamespace(foo)); } @@ -51,11 +51,11 @@ TEST(DispatchSmokeTest, TypeReflection) { Cpp::CreateInterpreter({}); Cpp::Declare("class DispType { int x; };"); - auto* scope = Cpp::GetNamed("DispType"); - auto* type = Cpp::GetTypeFromScope(scope); - EXPECT_NE(type, nullptr); + auto scope = Cpp::GetNamed("DispType"); + auto type = Cpp::GetTypeFromScope(scope); + EXPECT_TRUE(type); - auto* scope_back = Cpp::GetScopeFromType(type); + auto scope_back = Cpp::GetScopeFromType(type); EXPECT_EQ(scope, scope_back); EXPECT_GT(Cpp::SizeOf(scope), 0U); @@ -69,12 +69,12 @@ TEST(DispatchSmokeTest, FunctionReflection) { Cpp::CreateInterpreter({}); Cpp::Declare("namespace DispFn { int add(int a, int b) { return a+b; } }"); - auto* ns = Cpp::GetNamed("DispFn"); + auto ns = Cpp::GetNamed("DispFn"); auto fns = Cpp::GetFunctionsUsingName(ns, "add"); EXPECT_EQ(fns.size(), 1U); EXPECT_EQ(Cpp::GetFunctionNumArgs(fns[0]), 2U); - EXPECT_EQ(Cpp::GetName(fns[0]), "add"); + EXPECT_EQ(Cpp::GetName(Cpp::DeclRef{fns[0].data}), "add"); } // --- Variable reflection --- @@ -83,10 +83,10 @@ TEST(DispatchSmokeTest, VariableReflection) { Cpp::CreateInterpreter({}); Cpp::Declare("namespace DispVar { int gval = 99; }"); - auto* ns = Cpp::GetNamed("DispVar"); + auto ns = Cpp::GetNamed("DispVar"); // GetNamed finds variables inside a namespace scope. - auto* var = Cpp::GetNamed("gval", ns); - EXPECT_NE(var, nullptr); + auto var = Cpp::GetNamed("gval", ns); + EXPECT_TRUE(var); EXPECT_TRUE(Cpp::IsVariable(var)); } @@ -96,7 +96,7 @@ TEST(DispatchSmokeTest, TemplateInstantiation) { Cpp::CreateInterpreter({}); Cpp::Declare("template struct DispTmpl { T val; };"); - auto* tmpl = Cpp::GetNamed("DispTmpl"); + auto tmpl = Cpp::GetNamed("DispTmpl"); EXPECT_TRUE(Cpp::IsTemplate(tmpl)); } @@ -106,11 +106,11 @@ TEST(DispatchSmokeTest, ConstructDestruct) { Cpp::CreateInterpreter({"-include", "new"}); Cpp::Declare("struct DispObj { int x = 7; };"); - auto* scope = Cpp::GetNamed("DispObj"); + auto scope = Cpp::GetNamed("DispObj"); EXPECT_TRUE(Cpp::IsComplete(scope)); - auto* obj = Cpp::Construct(scope); - EXPECT_NE(obj, nullptr); + auto obj = Cpp::Construct(scope); + EXPECT_TRUE(obj); Cpp::Destruct(obj, scope, /*withFree=*/true); } @@ -120,7 +120,7 @@ TEST(DispatchSmokeTest, EnumReflection) { Cpp::CreateInterpreter({}); Cpp::Declare("enum DispEnum { A, B, C };"); - auto* e = Cpp::GetNamed("DispEnum"); + auto e = Cpp::GetNamed("DispEnum"); EXPECT_TRUE(Cpp::IsEnumScope(e)); auto constants = Cpp::GetEnumConstants(e); diff --git a/interpreter/CppInterOp/unittests/CppInterOp/DispatchTest.cpp b/interpreter/CppInterOp/unittests/CppInterOp/DispatchTest.cpp index 32c47f04c3555..157bb395c1bd1 100644 --- a/interpreter/CppInterOp/unittests/CppInterOp/DispatchTest.cpp +++ b/interpreter/CppInterOp/unittests/CppInterOp/DispatchTest.cpp @@ -14,13 +14,13 @@ TEST(DispatchTest, DlGetProcAddress_Basic) { Cpp::CreateInterpreter({}); Cpp::Declare("namespace N {} class C{}; int I;"); - using IsClassFn_t = bool (*)(Cpp::TCppScope_t); + using IsClassFn_t = bool (*)(Cpp::DeclRef); auto* IsClassFn = reinterpret_cast(dlGetProcAddress("IsClass")); EXPECT_NE(IsClassFn, nullptr) << "Failed to obtain API function pointer"; - auto* ns = Cpp::GetNamed("N"); - auto* cls = Cpp::GetNamed("C"); - auto* var = Cpp::GetNamed("I"); + auto ns = Cpp::GetNamed("N"); + auto cls = Cpp::GetNamed("C"); + auto var = Cpp::GetNamed("I"); EXPECT_FALSE(IsClassFn(ns)); EXPECT_TRUE(IsClassFn(cls)); diff --git a/interpreter/CppInterOp/unittests/CppInterOp/EnumReflectionTest.cpp b/interpreter/CppInterOp/unittests/CppInterOp/EnumReflectionTest.cpp index 19b58f3e7a15c..0e5d56025ba8d 100644 --- a/interpreter/CppInterOp/unittests/CppInterOp/EnumReflectionTest.cpp +++ b/interpreter/CppInterOp/unittests/CppInterOp/EnumReflectionTest.cpp @@ -208,7 +208,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, EnumReflection_GetEnumConstantType) { GetAllTopLevelDecls(code, Decls); - auto get_enum_constant_type_as_str = [](Cpp::TCppScope_t enum_constant) { + auto get_enum_constant_type_as_str = [](Cpp::DeclRef enum_constant) { return Cpp::GetTypeAsString(Cpp::GetEnumConstantType(enum_constant)); }; @@ -297,10 +297,10 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, EnumReflection_GetEnums) { TestFixture::CreateInterpreter(); Interp->declare(code); std::vector enumNames1, enumNames2, enumNames3, enumNames4; - Cpp::TCppScope_t globalscope = Cpp::GetScope("", 0); - Cpp::TCppScope_t Animals_scope = Cpp::GetScope("Animals", 0); - Cpp::TCppScope_t myClass_scope = Cpp::GetScope("myClass", 0); - Cpp::TCppScope_t unsupported_scope = Cpp::GetScope("myVariable", 0); + Cpp::DeclRef globalscope = Cpp::GetScope("", nullptr); + Cpp::DeclRef Animals_scope = Cpp::GetScope("Animals", nullptr); + Cpp::DeclRef myClass_scope = Cpp::GetScope("myClass", nullptr); + Cpp::DeclRef unsupported_scope = Cpp::GetScope("myVariable", nullptr); Cpp::GetEnums(globalscope, enumNames1); Cpp::GetEnums(Animals_scope, enumNames2); diff --git a/interpreter/CppInterOp/unittests/CppInterOp/FunctionReflectionTest.cpp b/interpreter/CppInterOp/unittests/CppInterOp/FunctionReflectionTest.cpp index 8b0771a7793d5..48e8609ede7cc 100644 --- a/interpreter/CppInterOp/unittests/CppInterOp/FunctionReflectionTest.cpp +++ b/interpreter/CppInterOp/unittests/CppInterOp/FunctionReflectionTest.cpp @@ -12,6 +12,7 @@ #include "gtest/gtest.h" +#include #include #include @@ -56,11 +57,11 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetClassMethods) { GetAllTopLevelDecls(code, Decls); - auto get_method_name = [](Cpp::TCppFunction_t method) { + auto get_method_name = [](Cpp::FuncRef method) { return Cpp::GetFunctionSignature(method); }; - std::vector methods0; + std::vector methods0; Cpp::GetClassMethods(Decls[0], methods0); EXPECT_EQ(methods0.size(), 11); @@ -76,7 +77,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetClassMethods) { EXPECT_EQ(get_method_name(methods0[9]), "inline constexpr A &A::operator=(A &&)"); EXPECT_EQ(get_method_name(methods0[10]), "inline A::~A()"); - std::vector methods1; + std::vector methods1; Cpp::GetClassMethods(Decls[2], methods1); EXPECT_EQ(methods0.size(), methods1.size()); EXPECT_EQ(methods0[0], methods1[0]); @@ -85,7 +86,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetClassMethods) { EXPECT_EQ(methods0[3], methods1[3]); EXPECT_EQ(methods0[4], methods1[4]); - std::vector methods2; + std::vector methods2; Cpp::GetClassMethods(Decls[3], methods2); EXPECT_EQ(methods2.size(), 6); @@ -96,7 +97,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetClassMethods) { EXPECT_EQ(get_method_name(methods2[4]), "inline B &B::operator=(const B &)"); EXPECT_EQ(get_method_name(methods2[5]), "inline B &B::operator=(B &&)"); - std::vector methods3; + std::vector methods3; Cpp::GetClassMethods(Decls[4], methods3); EXPECT_EQ(methods3.size(), 9); @@ -110,11 +111,11 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetClassMethods) { EXPECT_EQ(get_method_name(methods3[7]), "inline constexpr C::B(const B &)"); // Should not crash. - std::vector methods4; + std::vector methods4; Cpp::GetClassMethods(Decls[5], methods4); EXPECT_EQ(methods4.size(), 0); - std::vector methods5; + std::vector methods5; Cpp::GetClassMethods(nullptr, methods5); EXPECT_EQ(methods5.size(), 0); @@ -138,7 +139,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetClassMethods) { GetAllTopLevelDecls(code, Decls); EXPECT_EQ(Decls.size(), 2); - std::vector templ_methods1; + std::vector templ_methods1; Cpp::GetClassMethods(Decls[0], templ_methods1); EXPECT_EQ(templ_methods1.size(), 5); EXPECT_EQ(get_method_name(templ_methods1[0]), "T::T(const T &) = delete"); @@ -148,7 +149,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetClassMethods) { "inline T &T::operator=(const T &)"); EXPECT_EQ(get_method_name(templ_methods1[4]), "inline T::~T()"); - std::vector templ_methods2; + std::vector templ_methods2; Cpp::GetClassMethods(Decls[1], templ_methods2); EXPECT_EQ(templ_methods2.size(), 7); EXPECT_EQ(get_method_name(templ_methods2[0]), "void T::fn()"); @@ -175,7 +176,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, GetAllTopLevelDecls(code, Decls); auto has_constructor = [](Decl* D) { - std::vector methods; + std::vector methods; Cpp::GetClassMethods(D, methods); for (auto method : methods) { if (Cpp::IsConstructor(method)) @@ -267,14 +268,26 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetFunctionsUsingName) { } typedef A shadow_A; + + class Base { + public: + int g(int a) { return a; } + int g() { return 0; } + }; + + class Derived : public Base { + public: + using Base::g; + int h() { return 0; } + }; )"; GetAllTopLevelDecls(code, Decls); // This lambda can take in the scope and the name of the function // and returns the size of the vector returned by GetFunctionsUsingName - auto get_number_of_funcs_using_name = [&](Cpp::TCppScope_t scope, - const std::string &name) { + auto get_number_of_funcs_using_name = [&](Cpp::DeclRef scope, + const std::string& name) { auto Funcs = Cpp::GetFunctionsUsingName(scope, name); return Funcs.size(); @@ -289,6 +302,68 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetFunctionsUsingName) { EXPECT_EQ(get_number_of_funcs_using_name(Decls[2], "f2"), 1); EXPECT_EQ(get_number_of_funcs_using_name(Decls[2], "f3"), 1); EXPECT_EQ(get_number_of_funcs_using_name(Decls[2], ""), 0); + + Cpp::DeclRef derived = Cpp::GetScope("Derived"); + EXPECT_TRUE(derived); + EXPECT_EQ(get_number_of_funcs_using_name(derived, "g"), 2); + EXPECT_EQ(get_number_of_funcs_using_name(derived, "h"), 1); +} + +TYPED_TEST(CPPINTEROP_TEST_MODE, + FunctionReflection_GetFunctionsUsingNameOperators) { + std::vector Decls; + std::string code = R"( + namespace ComparableSpace { + class NSComparable {}; + bool operator==(const NSComparable&, const NSComparable&) { return true; } + bool operator==(const NSComparable&, int) { return false; } + bool operator!=(const NSComparable&, const NSComparable&) { return false; } + int operators_count() { return 1; } + } + + struct WithOps { + bool operator==(const WithOps&) const { return true; } + WithOps& operator+=(int) { return *this; } + int operator[](int) const { return 0; } + int operator()(int, int) const { return 0; } + }; + )"; + + GetAllTopLevelDecls(code, Decls); + + auto count = [](Cpp::DeclRef scope, const std::string& name) { + return Cpp::GetFunctionsUsingName(scope, name).size(); + }; + + // Namespace-scope operator overloads are now findable by name. + EXPECT_EQ(count(Decls[0], "operator=="), 2); + EXPECT_EQ(count(Decls[0], "operator!="), 1); + + // Non-existent operator at this scope. + EXPECT_EQ(count(Decls[0], "operator+"), 0); + + // "operator" followed by a non-identifier char that is not a valid operator + // spelling: this passes the early identifier-path check but matches no + // overloaded operator, so it must fall through to the empty DeclarationName + // and resolve (to nothing) via the identifier path. + EXPECT_EQ(count(Decls[0], "operator@"), 0); + + // Identifiers that merely start with "operator" must still resolve via the + // identifier lookup path, not be misread as an operator. + EXPECT_EQ(count(Decls[0], "operators_count"), 1); + + // Class-scope operators, including multi-token "()" and "[]". + EXPECT_EQ(count(Decls[1], "operator=="), 1); + EXPECT_EQ(count(Decls[1], "operator+="), 1); + EXPECT_EQ(count(Decls[1], "operator[]"), 1); + EXPECT_EQ(count(Decls[1], "operator()"), 1); + EXPECT_EQ(count(Decls[1], "operator!="), 0); + + // Sanity-check that the returned decls are the operator overloads. + auto eqs = Cpp::GetFunctionsUsingName(Decls[0], "operator=="); + ASSERT_EQ(eqs.size(), 2U); + for (auto f : eqs) + EXPECT_EQ(Cpp::GetName(Cpp::DeclRef{f.data}), "operator=="); } TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetClassDecls) { @@ -318,14 +393,46 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetClassDecls) { GetAllTopLevelDecls(code, Decls); GetAllSubDecls(Decls[0], SubDecls); - std::vector methods; + std::vector methods; Cpp::GetClassMethods(Decls[0], methods); EXPECT_EQ(methods.size(), 10); // includes structors and operators - EXPECT_EQ(Cpp::GetName(methods[0]), Cpp::GetName(SubDecls[4])); - EXPECT_EQ(Cpp::GetName(methods[1]), Cpp::GetName(SubDecls[5])); - EXPECT_EQ(Cpp::GetName(methods[2]), Cpp::GetName(SubDecls[7])); - EXPECT_EQ(Cpp::GetName(methods[3]), Cpp::GetName(SubDecls[8])); + EXPECT_EQ(Cpp::GetName(Cpp::DeclRef{methods[0].data}), + Cpp::GetName(SubDecls[4])); + EXPECT_EQ(Cpp::GetName(Cpp::DeclRef{methods[1].data}), + Cpp::GetName(SubDecls[5])); + EXPECT_EQ(Cpp::GetName(Cpp::DeclRef{methods[2].data}), + Cpp::GetName(SubDecls[7])); + EXPECT_EQ(Cpp::GetName(Cpp::DeclRef{methods[3].data}), + Cpp::GetName(SubDecls[8])); + + code = "class ForwardOnly;"; + methods.clear(); + Decls.clear(); + GetAllTopLevelDecls(code, Decls); + Cpp::GetClassMethods(Decls[0], methods); + EXPECT_EQ(methods.size(), 0); + + code = R"(template + class Klass; + template<> + class Klass;)"; + methods.clear(); + Decls.clear(); + GetAllTopLevelDecls(code, Decls); + Cpp::GetClassMethods(Decls[1], methods); + EXPECT_EQ(methods.size(), 0); + + code = R"(template + class Klass { + T value; + }; + using KlassInt = Klass;)"; + methods.clear(); + Decls.clear(); + GetAllTopLevelDecls(code, Decls); + Cpp::GetClassMethods(Decls[1], methods); + EXPECT_EQ(methods.size(), 6); // 6 implicit created } TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetFunctionTemplatedDecls) { @@ -355,14 +462,18 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetFunctionTemplatedDecls) { GetAllTopLevelDecls(code, Decls); GetAllSubDecls(Decls[0], SubDecls); - std::vector template_methods; + std::vector template_methods; Cpp::GetFunctionTemplatedDecls(Decls[0], template_methods); EXPECT_EQ(template_methods.size(), 4); - EXPECT_EQ(Cpp::GetName(template_methods[0]), Cpp::GetName(SubDecls[1])); - EXPECT_EQ(Cpp::GetName(template_methods[1]), Cpp::GetName(SubDecls[2])); - EXPECT_EQ(Cpp::GetName(template_methods[2]), Cpp::GetName(SubDecls[3])); - EXPECT_EQ(Cpp::GetName(template_methods[3]), Cpp::GetName(SubDecls[6])); + EXPECT_EQ(Cpp::GetName(Cpp::DeclRef{template_methods[0].data}), + Cpp::GetName(SubDecls[1])); + EXPECT_EQ(Cpp::GetName(Cpp::DeclRef{template_methods[1].data}), + Cpp::GetName(SubDecls[2])); + EXPECT_EQ(Cpp::GetName(Cpp::DeclRef{template_methods[2].data}), + Cpp::GetName(SubDecls[3])); + EXPECT_EQ(Cpp::GetName(Cpp::DeclRef{template_methods[3].data}), + Cpp::GetName(SubDecls[6])); } TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetFunctionReturnType) { @@ -450,16 +561,18 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetFunctionReturnType) { std::vector args = {C.IntTy.getAsOpaquePtr(), C.DoubleTy.getAsOpaquePtr()}; std::vector explicit_args; - std::vector candidates = {Decls[14]}; + std::vector candidates = {Decls[14]}; EXPECT_EQ( Cpp::GetTypeAsString(Cpp::GetFunctionReturnType( Cpp::BestOverloadFunctionMatch(candidates, explicit_args, args))), "RTTest_TemplatedList"); std::vector args2 = {C.DoubleTy.getAsOpaquePtr()}; - EXPECT_EQ(Cpp::GetTypeAsString(Cpp::GetFunctionReturnType(Cpp::GetNamed( - "func", Cpp::InstantiateTemplate(Decls[15], args2)))), - "double"); + EXPECT_EQ( + Cpp::GetTypeAsString(Cpp::GetFunctionReturnType(Cpp::FuncRef{ + Cpp::GetNamed("func", Cpp::InstantiateTemplate(Decls[15], args2)) + .data})), + "double"); } TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetFunctionNumArgs) { @@ -542,6 +655,8 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetFunctionArgType) { void f1(int i, double d, long l, char ch) {} void f2(const int i, double d[], long *l, char ch[4]) {} int a; + + template void f3(T t, const T& r, int i) {} )"; GetAllTopLevelDecls(code, Decls); @@ -549,11 +664,47 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetFunctionArgType) { EXPECT_EQ(Cpp::GetTypeAsString(Cpp::GetFunctionArgType(Decls[0], 1)), "double"); EXPECT_EQ(Cpp::GetTypeAsString(Cpp::GetFunctionArgType(Decls[0], 2)), "long"); EXPECT_EQ(Cpp::GetTypeAsString(Cpp::GetFunctionArgType(Decls[0], 3)), "char"); + EXPECT_EQ(Cpp::GetFunctionArgType(Decls[0], 4), nullptr); + EXPECT_EQ(Cpp::GetTypeAsString(Cpp::GetFunctionArgType(Decls[1], 0)), "const int"); EXPECT_EQ(Cpp::GetTypeAsString(Cpp::GetFunctionArgType(Decls[1], 1)), "double[]"); EXPECT_EQ(Cpp::GetTypeAsString(Cpp::GetFunctionArgType(Decls[1], 2)), "long *"); EXPECT_EQ(Cpp::GetTypeAsString(Cpp::GetFunctionArgType(Decls[1], 3)), "char[4]"); + EXPECT_EQ(Cpp::GetTypeAsString(Cpp::GetFunctionArgType(Decls[2], 0)), "NULL TYPE"); + + EXPECT_TRUE(Cpp::IsTemplatedFunction(Decls[3])); + EXPECT_EQ(Cpp::GetTypeAsString(Cpp::GetFunctionArgType(Decls[3], 0)), "T"); + EXPECT_EQ(Cpp::GetTypeAsString(Cpp::GetFunctionArgType(Decls[3], 1)), + "const T &"); + EXPECT_EQ(Cpp::GetTypeAsString(Cpp::GetFunctionArgType(Decls[3], 2)), "int"); +} + +TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_IsMethod) { + std::vector Decls, SubDecls; + std::string code = R"( + void f1() {} + + template void f2(T t) {} + + class MyClass { + void m1() {} + + template + void m2(T t) {} + }; + )"; + + GetAllTopLevelDecls(code, Decls); + GetAllSubDecls(Decls[2], SubDecls); + + EXPECT_FALSE(Cpp::IsMethod(Decls[0])); + EXPECT_FALSE(Cpp::IsMethod(Decls[1])); + EXPECT_FALSE(Cpp::IsMethod(SubDecls[0])); + EXPECT_TRUE(Cpp::IsMethod(SubDecls[1])); + EXPECT_TRUE(Cpp::IsTemplatedFunction(SubDecls[2])); + EXPECT_TRUE(Cpp::IsMethod(SubDecls[2])); + EXPECT_FALSE(Cpp::IsMethod(nullptr)); } TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_FunctionTypes) { @@ -567,7 +718,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_FunctionTypes) { GetAllTopLevelDecls(code, Decls); EXPECT_EQ(Decls.size(), 3); - Cpp::TCppType_t typ1 = Cpp::GetVariableType(Decls[1]); + Cpp::TypeRef typ1 = Cpp::GetVariableType(Decls[1]); EXPECT_TRUE(typ1); EXPECT_FALSE(Cpp::IsFunctionProtoType(typ1)); EXPECT_TRUE(Cpp::IsFunctionProtoType(Cpp::GetPointeeType(typ1))); @@ -575,14 +726,14 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_FunctionTypes) { typ1 = Cpp::GetPointeeType(typ1); EXPECT_TRUE(typ1); - std::vector sig; + std::vector sig; Cpp::GetFnTypeSignature(typ1, sig); EXPECT_EQ(sig.size(), 3); EXPECT_EQ(Cpp::GetTypeAsString(sig[0]), "int"); EXPECT_EQ(Cpp::GetTypeAsString(sig[1]), "int"); EXPECT_EQ(Cpp::GetTypeAsString(sig[2]), "double"); - Cpp::TCppType_t typ2 = Cpp::GetFunctionArgType(Decls[2], 0); + Cpp::TypeRef typ2 = Cpp::GetFunctionArgType(Decls[2], 0); EXPECT_TRUE(typ1); typ2 = Cpp::GetNonReferenceType(typ2); @@ -686,7 +837,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_ExistsFunctionTemplate) { )"; GetAllTopLevelDecls(code, Decls); - EXPECT_TRUE(Cpp::ExistsFunctionTemplate("f", 0)); + EXPECT_TRUE(Cpp::ExistsFunctionTemplate("f", nullptr)); EXPECT_TRUE(Cpp::ExistsFunctionTemplate("f", Decls[1])); EXPECT_FALSE(Cpp::ExistsFunctionTemplate("f", Decls[2])); } @@ -698,7 +849,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, std::string code = R"(#include )"; Interp->process(code); const char* str = "std::make_unique"; - auto* Instance1 = (Decl*)Cpp::InstantiateTemplateFunctionFromString(str); + auto Instance1 = Cpp::InstantiateTemplateFunctionFromString(str); EXPECT_TRUE(Instance1); } @@ -714,8 +865,8 @@ template T TrivialFnTemplate() { return T(); } std::vector args1 = {C.IntTy.getAsOpaquePtr()}; auto Instance1 = Cpp::InstantiateTemplate(Decls[0], args1); - EXPECT_TRUE(isa((Decl*)Instance1)); - FunctionDecl* FD = cast((Decl*)Instance1); + EXPECT_TRUE(isa(Cpp::unwrap(Instance1))); + FunctionDecl* FD = cast(Cpp::unwrap(Instance1)); FunctionDecl* FnTD1 = FD->getTemplateInstantiationPattern(); EXPECT_TRUE(FnTD1->isThisDeclarationADefinition()); TemplateArgument TA1 = FD->getTemplateSpecializationArgs()->get(0); @@ -741,8 +892,8 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_InstantiateTemplateMethod) { std::vector args1 = {C.IntTy.getAsOpaquePtr()}; auto Instance1 = Cpp::InstantiateTemplate(Decls[1], args1); - EXPECT_TRUE(isa((Decl*)Instance1)); - FunctionDecl* FD = cast((Decl*)Instance1); + EXPECT_TRUE(isa(Cpp::unwrap(Instance1))); + FunctionDecl* FD = cast(Cpp::unwrap(Instance1)); FunctionDecl* FnTD1 = FD->getTemplateInstantiationPattern(); EXPECT_TRUE(FnTD1->isThisDeclarationADefinition()); TemplateArgument TA1 = FD->getTemplateSpecializationArgs()->get(0); @@ -775,7 +926,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_LookupConstructors) { )"; GetAllTopLevelDecls(code, Decls); - std::vector ctors; + std::vector ctors; Cpp::LookupConstructors("MyClass", Decls[0], ctors); EXPECT_EQ(ctors.size(), 4) @@ -819,10 +970,22 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetClassTemplatedMethods) { void MyClass::staticFunc() {} template void MyClass::templatedStaticMethod(T param) {} + + class TBase { + public: + template + void usedTemplated(T param); + template + void usedTemplated(T t, U u); + }; + class TDerived : public TBase { + public: + using TBase::usedTemplated; + }; )"; GetAllTopLevelDecls(code, Decls); - std::vector templatedMethods; + std::vector templatedMethods; Cpp::GetClassTemplatedMethods("MyClass", Decls[0], templatedMethods); Cpp::GetClassTemplatedMethods("templatedMethod", Decls[0], templatedMethods); Cpp::GetClassTemplatedMethods("templatedStaticMethod", Decls[0], @@ -842,6 +1005,16 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetClassTemplatedMethods) { "U MyClass::templatedMethod(U a, V b)"); EXPECT_EQ(Cpp::GetFunctionSignature(templatedMethods[5]), "void MyClass::templatedStaticMethod(T param)"); + + std::vector usingMethods; + Cpp::DeclRef derived = Cpp::GetScope("TDerived"); + EXPECT_TRUE(derived); + Cpp::GetClassTemplatedMethods("usedTemplated", derived, usingMethods); + EXPECT_EQ(usingMethods.size(), 2); + EXPECT_EQ(Cpp::GetFunctionSignature(usingMethods[0]), + "void TBase::usedTemplated(T param)"); + EXPECT_EQ(Cpp::GetFunctionSignature(usingMethods[1]), + "void TBase::usedTemplated(T t, U u)"); } TYPED_TEST(CPPINTEROP_TEST_MODE, @@ -879,7 +1052,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, )"; GetAllTopLevelDecls(code, Decls); - std::vector templatedMethods; + std::vector templatedMethods; Cpp::GetClassTemplatedMethods("fixedMethod", Decls[0], templatedMethods); Cpp::GetClassTemplatedMethods("defaultMethod", Decls[0], templatedMethods); Cpp::GetClassTemplatedMethods("variadicMethod", Decls[0], templatedMethods); @@ -918,11 +1091,11 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, std::vector args1 = {C.DoubleTy.getAsOpaquePtr(), C.IntTy.getAsOpaquePtr()}; auto Instance1 = Cpp::InstantiateTemplate(Decls[1], args1); - EXPECT_TRUE(Cpp::IsTemplatedFunction(Instance1)); - EXPECT_EQ(Cpp::GetFunctionSignature(Instance1), + EXPECT_TRUE(Cpp::IsTemplatedFunction(Cpp::FuncRef{Instance1.data})); + EXPECT_EQ(Cpp::GetFunctionSignature(Cpp::FuncRef{Instance1.data}), "template<> void VariadicFn<>(double args, int args)"); - FunctionDecl* FD = cast((Decl*)Instance1); + FunctionDecl* FD = cast(Cpp::unwrap(Instance1)); FunctionDecl* FnTD1 = FD->getTemplateInstantiationPattern(); EXPECT_TRUE(FnTD1->isThisDeclarationADefinition()); EXPECT_EQ(FD->getNumParams(), 2); @@ -935,14 +1108,14 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, // handle to MyClass type auto MyClassType = Cpp::GetTypeFromScope(Decls[0]); - std::vector args2 = {MyClassType, + std::vector args2 = {MyClassType.data, C.DoubleTy.getAsOpaquePtr()}; // instantiate VariadicFnExtended auto Instance2 = Cpp::InstantiateTemplate(Decls[2], args2, true); - EXPECT_TRUE(Cpp::IsTemplatedFunction(Instance2)); + EXPECT_TRUE(Cpp::IsTemplatedFunction(Cpp::FuncRef{Instance2.data})); - FunctionDecl* FD2 = cast((Decl*)Instance2); + FunctionDecl* FD2 = cast(Cpp::unwrap(Instance2)); FunctionDecl* FnTD2 = FD2->getTemplateInstantiationPattern(); EXPECT_TRUE(FnTD2->isThisDeclarationADefinition()); @@ -956,7 +1129,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, EXPECT_TRUE(PackArgs2[0].getAsType()->isRecordType()); // MyClass EXPECT_TRUE(PackArgs2[1].getAsType()->isFloatingType()); // double - EXPECT_EQ(Cpp::GetFunctionSignature(Instance2), + EXPECT_EQ(Cpp::GetFunctionSignature(Cpp::FuncRef{Instance2.data}), "template<> void VariadicFnExtended<>(int " "fixedParam, MyClass args, double args)"); } @@ -972,7 +1145,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, GetAllTopLevelDecls(code, Decls); EXPECT_EQ(Decls.size(), 1); - std::vector candidates; + std::vector candidates; candidates.reserve(Decls.size()); for (auto* i : Decls) candidates.push_back(i); @@ -992,21 +1165,21 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, C.IntTy.getAsOpaquePtr(), }; - Cpp::TCppScope_t fn0 = + Cpp::FuncRef fn0 = Cpp::BestOverloadFunctionMatch(candidates, explicit_args0, args0); EXPECT_TRUE(fn0); - Cpp::TCppScope_t fn = + Cpp::FuncRef fn = Cpp::BestOverloadFunctionMatch(candidates, explicit_args1, args0); EXPECT_EQ(fn, fn0); fn = Cpp::BestOverloadFunctionMatch(candidates, explicit_args2, args0); EXPECT_EQ(fn, fn0); - fn = Cpp::InstantiateTemplate(Decls[0], explicit_args1); + fn = Cpp::FuncRef{Cpp::InstantiateTemplate(Decls[0], explicit_args1).data}; EXPECT_EQ(fn, fn0); - fn = Cpp::InstantiateTemplate(Decls[0], explicit_args2); + fn = Cpp::FuncRef{Cpp::InstantiateTemplate(Decls[0], explicit_args2).data}; EXPECT_EQ(fn, fn0); } @@ -1050,10 +1223,11 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, )"; GetAllTopLevelDecls(code, Decls); - std::vector candidates; + std::vector candidates; for (auto decl : Decls) - if (Cpp::IsTemplatedFunction(decl)) candidates.push_back((Cpp::TCppFunction_t)decl); + if (Cpp::IsTemplatedFunction(decl)) + candidates.push_back((Cpp::FuncRef)decl); ASTContext& C = Interp->getCI()->getASTContext(); @@ -1068,15 +1242,15 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, std::vector explicit_args2 = { {C.IntTy.getAsOpaquePtr(), "1"}, C.IntTy.getAsOpaquePtr()}; - Cpp::TCppFunction_t func1 = + Cpp::FuncRef func1 = Cpp::BestOverloadFunctionMatch(candidates, explicit_args0, args1); - Cpp::TCppFunction_t func2 = + Cpp::FuncRef func2 = Cpp::BestOverloadFunctionMatch(candidates, explicit_args1, args0); - Cpp::TCppFunction_t func3 = + Cpp::FuncRef func3 = Cpp::BestOverloadFunctionMatch(candidates, explicit_args0, args2); - Cpp::TCppFunction_t func4 = + Cpp::FuncRef func4 = Cpp::BestOverloadFunctionMatch(candidates, explicit_args1, args3); - Cpp::TCppFunction_t func5 = + Cpp::FuncRef func5 = Cpp::BestOverloadFunctionMatch(candidates, explicit_args2, args3); EXPECT_EQ(Cpp::GetFunctionSignature(func1), @@ -1116,11 +1290,11 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, )"; GetAllTopLevelDecls(code, Decls); - std::vector candidates; + std::vector candidates; for (auto decl : Decls) if (Cpp::IsFunction(decl) || Cpp::IsTemplatedFunction(decl)) - candidates.push_back((Cpp::TCppFunction_t)decl); + candidates.push_back((Cpp::FuncRef)decl); EXPECT_EQ(candidates.size(), 5); @@ -1128,26 +1302,26 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, std::vector args1 = {C.IntTy.getAsOpaquePtr()}; std::vector args2 = { - Cpp::GetVariableType(Cpp::GetNamed("a"))}; + Cpp::GetVariableType(Cpp::GetNamed("a")).data}; std::vector args3 = {C.IntTy.getAsOpaquePtr(), C.IntTy.getAsOpaquePtr()}; std::vector args4 = { - Cpp::GetVariableType(Cpp::GetNamed("a")), - Cpp::GetVariableType(Cpp::GetNamed("a"))}; + Cpp::GetVariableType(Cpp::GetNamed("a")).data, + Cpp::GetVariableType(Cpp::GetNamed("a")).data}; std::vector args5 = {C.IntTy.getAsOpaquePtr(), C.DoubleTy.getAsOpaquePtr()}; std::vector explicit_args; - Cpp::TCppFunction_t func1 = + Cpp::FuncRef func1 = Cpp::BestOverloadFunctionMatch(candidates, explicit_args, args1); - Cpp::TCppFunction_t func2 = + Cpp::FuncRef func2 = Cpp::BestOverloadFunctionMatch(candidates, explicit_args, args2); - Cpp::TCppFunction_t func3 = + Cpp::FuncRef func3 = Cpp::BestOverloadFunctionMatch(candidates, explicit_args, args3); - Cpp::TCppFunction_t func4 = + Cpp::FuncRef func4 = Cpp::BestOverloadFunctionMatch(candidates, explicit_args, args4); - Cpp::TCppFunction_t func5 = + Cpp::FuncRef func5 = Cpp::BestOverloadFunctionMatch(candidates, explicit_args, args5); EXPECT_EQ(Cpp::GetFunctionSignature(func1), @@ -1190,30 +1364,31 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, )"; GetAllTopLevelDecls(code, Decls); - std::vector candidates; + std::vector candidates; for (auto decl : Decls) if (Cpp::IsTemplatedFunction(decl)) - candidates.push_back((Cpp::TCppFunction_t)decl); + candidates.push_back((Cpp::FuncRef)decl); EXPECT_EQ(candidates.size(), 2); ASTContext& C = Interp->getCI()->getASTContext(); std::vector args1 = { - Cpp::GetVariableType(Cpp::GetNamed("a")), - Cpp::GetVariableType(Cpp::GetNamed("a"))}; + Cpp::GetVariableType(Cpp::GetNamed("a")).data, + Cpp::GetVariableType(Cpp::GetNamed("a")).data}; std::vector args2 = { - Cpp::GetVariableType(Cpp::GetNamed("a")), C.IntTy.getAsOpaquePtr()}; + Cpp::GetVariableType(Cpp::GetNamed("a")).data, C.IntTy.getAsOpaquePtr()}; std::vector args3 = { - Cpp::GetVariableType(Cpp::GetNamed("a")), C.DoubleTy.getAsOpaquePtr()}; + Cpp::GetVariableType(Cpp::GetNamed("a")).data, + C.DoubleTy.getAsOpaquePtr()}; std::vector explicit_args; - Cpp::TCppFunction_t func1 = + Cpp::FuncRef func1 = Cpp::BestOverloadFunctionMatch(candidates, explicit_args, args1); - Cpp::TCppFunction_t func2 = + Cpp::FuncRef func2 = Cpp::BestOverloadFunctionMatch(candidates, explicit_args, args2); - Cpp::TCppFunction_t func3 = + Cpp::FuncRef func3 = Cpp::BestOverloadFunctionMatch(candidates, explicit_args, args3); candidates.clear(); @@ -1224,9 +1399,9 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, EXPECT_EQ(candidates.size(), 1); std::vector args4 = { - Cpp::GetVariableType(Cpp::GetNamed("a"))}; + Cpp::GetVariableType(Cpp::GetNamed("a")).data}; - Cpp::TCppFunction_t func4 = + Cpp::FuncRef func4 = Cpp::BestOverloadFunctionMatch(candidates, explicit_args, args4); EXPECT_EQ(Cpp::GetFunctionSignature(func1), @@ -1264,7 +1439,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, GetAllTopLevelDecls(code, Decls); GetAllSubDecls(Decls[1], SubDecls); - std::vector candidates; + std::vector candidates; for (auto i : SubDecls) { if ((Cpp::IsFunction(i) || Cpp::IsTemplatedFunction(i)) && Cpp::GetName(i) == "fn") @@ -1278,27 +1453,27 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, std::vector args1 = {}; std::vector args2 = {C.IntTy.getAsOpaquePtr()}; std::vector args3 = { - Cpp::GetVariableType(Cpp::GetNamed("a"))}; + Cpp::GetVariableType(Cpp::GetNamed("a")).data}; std::vector args4 = { - Cpp::GetVariableType(Cpp::GetNamed("a")), - Cpp::GetVariableType(Cpp::GetNamed("b"))}; + Cpp::GetVariableType(Cpp::GetNamed("a")).data, + Cpp::GetVariableType(Cpp::GetNamed("b")).data}; std::vector args5 = { - Cpp::GetVariableType(Cpp::GetNamed("a")), - Cpp::GetVariableType(Cpp::GetNamed("a"))}; + Cpp::GetVariableType(Cpp::GetNamed("a")).data, + Cpp::GetVariableType(Cpp::GetNamed("a")).data}; std::vector explicit_args1; std::vector explicit_args2 = {C.IntTy.getAsOpaquePtr(), C.IntTy.getAsOpaquePtr()}; - Cpp::TCppFunction_t func1 = + Cpp::FuncRef func1 = Cpp::BestOverloadFunctionMatch(candidates, explicit_args1, args1); - Cpp::TCppFunction_t func2 = + Cpp::FuncRef func2 = Cpp::BestOverloadFunctionMatch(candidates, explicit_args1, args2); - Cpp::TCppFunction_t func3 = + Cpp::FuncRef func3 = Cpp::BestOverloadFunctionMatch(candidates, explicit_args1, args3); - Cpp::TCppFunction_t func4 = + Cpp::FuncRef func4 = Cpp::BestOverloadFunctionMatch(candidates, explicit_args1, args4); - Cpp::TCppFunction_t func5 = + Cpp::FuncRef func5 = Cpp::BestOverloadFunctionMatch(candidates, explicit_args2, args5); EXPECT_EQ(Cpp::GetFunctionSignature(func1), "void B::fn()"); @@ -1327,7 +1502,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, GetAllTopLevelDecls(code, Decls); EXPECT_EQ(Decls.size(), 2); - std::vector candidates; + std::vector candidates; candidates.push_back(Decls[1]); ASTContext& C = Interp->getCI()->getASTContext(); @@ -1336,16 +1511,16 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, C.IntTy.getAsOpaquePtr(), }; - Cpp::TCppScope_t callme = Cpp::InstantiateTemplate(Decls[0], explicit_params); + Cpp::DeclRef callme = Cpp::InstantiateTemplate(Decls[0], explicit_params); EXPECT_TRUE(callme); std::vector arg_types = { - Cpp::GetTypeFromScope(callme), + Cpp::GetTypeFromScope(callme).data, C.getLValueReferenceType(C.DoubleTy).getAsOpaquePtr(), C.getLValueReferenceType(C.IntTy).getAsOpaquePtr(), }; - Cpp::TCppScope_t callback = + Cpp::FuncRef callback = Cpp::BestOverloadFunctionMatch(candidates, empty_templ_args, arg_types); EXPECT_TRUE(callback); @@ -1553,23 +1728,25 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetFunctionAddress) { address << Cpp::GetFunctionAddress(Decls[0]); EXPECT_EQ(address.str(), output); - EXPECT_FALSE(Cpp::GetFunctionAddress(Cpp::GetGlobalScope())); + EXPECT_FALSE( + Cpp::GetFunctionAddress(Cpp::FuncRef{Cpp::GetGlobalScope().data})); Interp->declare(R"( template T add1(T t) { return t + 1; } )"); - std::vector funcs; + std::vector funcs; Cpp::GetClassTemplatedMethods("add1", Cpp::GetGlobalScope(), funcs); EXPECT_EQ(funcs.size(), 1); ASTContext& C = Interp->getCI()->getASTContext(); std::vector argument = {C.DoubleTy.getAsOpaquePtr()}; - Cpp::TCppScope_t add1_double = Cpp::InstantiateTemplate(funcs[0], argument); + Cpp::DeclRef add1_double = + Cpp::InstantiateTemplate(Cpp::DeclRef{funcs[0].data}, argument); EXPECT_TRUE(add1_double); - EXPECT_TRUE(Cpp::GetFunctionAddress(add1_double)); + EXPECT_TRUE(Cpp::GetFunctionAddress(Cpp::FuncRef{add1_double.data})); } TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_IsVirtualMethod) { @@ -1619,8 +1796,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_JitCallAdvanced) { GetAllTopLevelDecls(code, Decls, /*filter_implicitGenerated=*/false, interpreter_args); - auto *CtorD - = (clang::CXXConstructorDecl*)Cpp::GetDefaultConstructor(Decls[0]); + auto CtorD = Cpp::GetDefaultConstructor(Decls[0]); auto Ctor = Cpp::MakeFunctionCallable(CtorD); EXPECT_TRUE((bool)Ctor) << "Failed to build a wrapper for the ctor"; void* object = nullptr; @@ -1651,7 +1827,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_JitCallDebug) { GetAllTopLevelDecls(code, Decls, /*filter_implicitGenerated=*/false, interpreter_args); - const auto* CtorD = Cpp::GetDefaultConstructor(Decls[0]); + const auto CtorD = Cpp::GetDefaultConstructor(Decls[0]); auto JC = Cpp::MakeFunctionCallable(CtorD); EXPECT_TRUE(JC.getKind() == Cpp::JitCall::kConstructorCall); @@ -1659,7 +1835,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_JitCallDebug) { { JC.InvokeConstructor(/*result=*/nullptr); }, "Must pass the location of the created object!"); - void* result = Cpp::Allocate(Decls[0]); + void* result = Cpp::Allocate(Decls[0]).data; EXPECT_DEATH( { JC.InvokeConstructor(&result, 0UL); }, "Number of objects to construct should be atleast 1"); @@ -1692,7 +1868,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_JitCallDebug) { JC = Cpp::MakeFunctionCallable(SubDecls[3]); EXPECT_TRUE(JC.getKind() == Cpp::JitCall::kConstructorCall); - result = Cpp::Allocate(Decls[0], 5); + result = Cpp::Allocate(Decls[0], 5).data; int i = 42; void* args0[1] = {(void*)&i}; EXPECT_DEATH( @@ -1705,8 +1881,8 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_JitCallDebug) { EXPECT_TRUE(*obj == 42); // Destructors - Cpp::TCppScope_t scope_C = Cpp::GetNamed("C"); - Cpp::TCppObject_t object_C = Cpp::Construct(scope_C); + Cpp::DeclRef scope_C = Cpp::GetNamed("C"); + Cpp::ObjectRef object_C = Cpp::Construct(scope_C); // Make destructor callable and pass arguments JC = Cpp::MakeFunctionCallable(SubDecls[4]); @@ -1771,13 +1947,14 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetFunctionCallWrapper) { Cpp::JitCall FCI1 = Cpp::MakeFunctionCallable(Decls[0]); EXPECT_TRUE(FCI1.getKind() == Cpp::JitCall::kGenericCall); - Cpp::JitCall FCI2 = Cpp::MakeFunctionCallable(Cpp::GetNamed("f2")); + Cpp::JitCall FCI2 = + Cpp::MakeFunctionCallable(Cpp::FuncRef{Cpp::GetNamed("f2").data}); EXPECT_TRUE(FCI2.getKind() == Cpp::JitCall::kGenericCall); - Cpp::JitCall FCI3 = - Cpp::MakeFunctionCallable(Cpp::GetNamed("f3", Cpp::GetNamed("NS"))); + Cpp::JitCall FCI3 = Cpp::MakeFunctionCallable( + Cpp::FuncRef{Cpp::GetNamed("f3", Cpp::GetNamed("NS")).data}); EXPECT_TRUE(FCI3.getKind() == Cpp::JitCall::kGenericCall); - Cpp::JitCall FCI4 = - Cpp::MakeFunctionCallable(Cpp::GetNamed("f4", Cpp::GetNamed("NS"))); + Cpp::JitCall FCI4 = Cpp::MakeFunctionCallable( + Cpp::FuncRef{Cpp::GetNamed("f4", Cpp::GetNamed("NS")).data}); EXPECT_TRUE(FCI4.getKind() == Cpp::JitCall::kGenericCall); int i = 9, ret1, ret3, ret4; @@ -1799,8 +1976,8 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetFunctionCallWrapper) { FCI4.Invoke(&ret4); EXPECT_EQ(ret4, 4); - Cpp::JitCall FCI5 = - Cpp::MakeFunctionCallable(Cpp::GetNamed("f5", Cpp::GetNamed("NS"))); + Cpp::JitCall FCI5 = Cpp::MakeFunctionCallable( + Cpp::FuncRef{Cpp::GetNamed("f5", Cpp::GetNamed("NS")).data}); EXPECT_TRUE(FCI5.getKind() == Cpp::JitCall::kGenericCall); typedef int (*int_func)(); @@ -1818,8 +1995,8 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetFunctionCallWrapper) { }; )"); - clang::NamedDecl* ClassC = (clang::NamedDecl*)Cpp::GetNamed("C"); - auto *CtorD = (clang::CXXConstructorDecl*)Cpp::GetDefaultConstructor(ClassC); + auto ClassC = Cpp::GetNamed("C"); + auto CtorD = Cpp::GetDefaultConstructor(ClassC); auto FCI_Ctor = Cpp::MakeFunctionCallable(CtorD); void* object = nullptr; @@ -1827,9 +2004,9 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetFunctionCallWrapper) { FCI_Ctor.Invoke((void*)&object); output = testing::internal::GetCapturedStdout(); EXPECT_EQ(output, "Default Ctor Called\n"); - EXPECT_TRUE(object != nullptr); + EXPECT_TRUE(object); - auto *DtorD = (clang::CXXDestructorDecl*)Cpp::GetDestructor(ClassC); + auto DtorD = Cpp::GetDestructor(ClassC); auto FCI_Dtor = Cpp::MakeFunctionCallable(DtorD); testing::internal::CaptureStdout(); @@ -1853,10 +2030,12 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetFunctionCallWrapper) { std::vector argument = {C.IntTy.getAsOpaquePtr()}; auto Instance1 = Cpp::InstantiateTemplate(Decls1[0], argument); - EXPECT_TRUE(isa((Decl*)Instance1)); - auto* CTSD1 = static_cast(Instance1); - auto* Add_D = Cpp::GetNamed("Add", CTSD1); - Cpp::JitCall FCI_Add = Cpp::MakeFunctionCallable(Add_D); + EXPECT_TRUE( + isa(Cpp::unwrap(Instance1))); + auto* CTSD1 = + cast(Cpp::unwrap(Instance1)); + auto Add_D = Cpp::GetNamed("Add", CTSD1); + Cpp::JitCall FCI_Add = Cpp::MakeFunctionCallable(Cpp::FuncRef{Add_D.data}); EXPECT_TRUE(FCI_Add.getKind() == Cpp::JitCall::kGenericCall); int a = 5, b = 10, result; @@ -1872,10 +2051,10 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetFunctionCallWrapper) { } )"); - Cpp::TCppScope_t set_5 = Cpp::GetNamed("set_5"); + Cpp::DeclRef set_5 = Cpp::GetNamed("set_5"); EXPECT_TRUE(set_5); - Cpp::JitCall set_5_f = Cpp::MakeFunctionCallable(set_5); + Cpp::JitCall set_5_f = Cpp::MakeFunctionCallable(Cpp::FuncRef{set_5.data}); EXPECT_EQ(set_5_f.getKind(), Cpp::JitCall::kGenericCall); int* bp = &b; @@ -1897,14 +2076,13 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetFunctionCallWrapper) { }; )"); - Cpp::TCppScope_t TypedefToPrivateClass = - Cpp::GetNamed("TypedefToPrivateClass"); + Cpp::DeclRef TypedefToPrivateClass = Cpp::GetNamed("TypedefToPrivateClass"); EXPECT_TRUE(TypedefToPrivateClass); - Cpp::TCppScope_t f = Cpp::GetNamed("f", TypedefToPrivateClass); + Cpp::DeclRef f = Cpp::GetNamed("f", TypedefToPrivateClass); EXPECT_TRUE(f); - Cpp::JitCall FCI_f = Cpp::MakeFunctionCallable(f); + Cpp::JitCall FCI_f = Cpp::MakeFunctionCallable(Cpp::FuncRef{f.data}); EXPECT_EQ(FCI_f.getKind(), Cpp::JitCall::kGenericCall); void* res = nullptr; @@ -1919,22 +2097,22 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetFunctionCallWrapper) { bool operator<(T t) { return true; } }; )"); - Cpp::TCppScope_t TOperator = Cpp::GetNamed("TOperator"); + Cpp::DeclRef TOperator = Cpp::GetNamed("TOperator"); - auto* TOperatorCtor = Cpp::GetDefaultConstructor(TOperator); + auto TOperatorCtor = Cpp::GetDefaultConstructor(TOperator); auto FCI_TOperatorCtor = Cpp::MakeFunctionCallable(TOperatorCtor); void* toperator = nullptr; FCI_TOperatorCtor.Invoke((void*)&toperator); EXPECT_TRUE(toperator); - std::vector operators; + std::vector operators; Cpp::GetOperator(TOperator, Cpp::Operator::OP_Less, operators); EXPECT_EQ(operators.size(), 1); - Cpp::TCppScope_t op_templated = operators[0]; - auto TAI = Cpp::TemplateArgInfo(Cpp::GetType("int")); - Cpp::TCppScope_t op = Cpp::InstantiateTemplate(op_templated, {TAI}); - auto FCI_op = Cpp::MakeFunctionCallable(op); + Cpp::DeclRef op_templated{operators[0].data}; + auto TAI = Cpp::TemplateArgInfo(Cpp::GetType("int").data); + Cpp::DeclRef op = Cpp::InstantiateTemplate(op_templated, {TAI}); + auto FCI_op = Cpp::MakeFunctionCallable(Cpp::FuncRef{op.data}); bool boolean = false; FCI_op.Invoke((void*)&boolean, {args, /*args_size=*/1}, toperator); EXPECT_TRUE(boolean); @@ -1969,14 +2147,14 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetFunctionCallWrapper) { N1::N2::Klass1 K2; )"); - Cpp::TCppType_t K1 = Cpp::GetTypeFromScope(Cpp::GetNamed("K1")); - Cpp::TCppType_t K2 = Cpp::GetTypeFromScope(Cpp::GetNamed("K2")); + Cpp::TypeRef K1 = Cpp::GetTypeFromScope(Cpp::GetNamed("K1")); + Cpp::TypeRef K2 = Cpp::GetTypeFromScope(Cpp::GetNamed("K2")); operators.clear(); Cpp::GetOperator(Cpp::GetScope("N2", Cpp::GetScope("N1")), Cpp::Operator::OP_Plus, operators); EXPECT_EQ(operators.size(), 1); - Cpp::TCppFunction_t kop = - Cpp::BestOverloadFunctionMatch(operators, empty_templ_args, {K1, K2}); + Cpp::FuncRef kop = Cpp::BestOverloadFunctionMatch(operators, empty_templ_args, + {K1.data, K2.data}); auto chrono_op_fn_callable = Cpp::MakeFunctionCallable(kop); EXPECT_EQ(chrono_op_fn_callable.getKind(), Cpp::JitCall::kGenericCall); @@ -2048,14 +2226,15 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetFunctionCallWrapper) { auto &p = tmp; )"); - std::vector unresolved_candidate_methods; + std::vector unresolved_candidate_methods; Cpp::GetClassTemplatedMethods("get", Cpp::GetScope("my_std"), unresolved_candidate_methods); - Cpp::TCppType_t p = Cpp::GetTypeFromScope(Cpp::GetNamed("p")); + Cpp::TypeRef p = Cpp::GetTypeFromScope(Cpp::GetNamed("p")); EXPECT_TRUE(p); - Cpp::TCppScope_t fn = Cpp::BestOverloadFunctionMatch( - unresolved_candidate_methods, {{Cpp::GetType("int"), "0"}}, {p}); + Cpp::FuncRef fn = Cpp::BestOverloadFunctionMatch( + unresolved_candidate_methods, {{Cpp::GetType("int").data, "0"}}, + {p.data}); EXPECT_TRUE(fn); auto fn_callable = Cpp::MakeFunctionCallable(fn); @@ -2073,9 +2252,9 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetFunctionCallWrapper) { unresolved_candidate_methods); EXPECT_EQ(unresolved_candidate_methods.size(), 1); - Cpp::TCppScope_t call_move = Cpp::BestOverloadFunctionMatch( + Cpp::FuncRef call_move = Cpp::BestOverloadFunctionMatch( unresolved_candidate_methods, {}, - {Cpp::GetReferencedType(Cpp::GetType("int"), true)}); + {Cpp::GetReferencedType(Cpp::GetType("int"), true).data}); EXPECT_TRUE(call_move); auto call_move_callable = Cpp::MakeFunctionCallable(call_move); @@ -2089,8 +2268,8 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetFunctionCallWrapper) { unresolved_candidate_methods); EXPECT_EQ(unresolved_candidate_methods.size(), 1); - Cpp::TCppScope_t instantiation_in_host = Cpp::BestOverloadFunctionMatch( - unresolved_candidate_methods, {Cpp::GetType("int")}, {}); + Cpp::FuncRef instantiation_in_host = Cpp::BestOverloadFunctionMatch( + unresolved_candidate_methods, {Cpp::GetType("int").data}, {}); EXPECT_TRUE(instantiation_in_host); Cpp::JitCall instantiation_in_host_callable = @@ -2099,7 +2278,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetFunctionCallWrapper) { Cpp::JitCall::kGenericCall); instantiation_in_host = Cpp::BestOverloadFunctionMatch( - unresolved_candidate_methods, {Cpp::GetType("double")}, {}); + unresolved_candidate_methods, {Cpp::GetType("double").data}, {}); EXPECT_TRUE(instantiation_in_host); Cpp::BeginStdStreamCapture(Cpp::CaptureStreamKind::kStdErr); @@ -2126,10 +2305,10 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetFunctionCallWrapper) { unresolved_candidate_methods); EXPECT_EQ(unresolved_candidate_methods.size(), 1); - Cpp::TCppScope_t tuple_tuple = Cpp::BestOverloadFunctionMatch( + Cpp::FuncRef tuple_tuple = Cpp::BestOverloadFunctionMatch( unresolved_candidate_methods, {}, - {Cpp::GetVariableType(Cpp::GetNamed("tuple_one")), - Cpp::GetVariableType(Cpp::GetNamed("tuple_two"))}); + {Cpp::GetVariableType(Cpp::GetNamed("tuple_one")).data, + Cpp::GetVariableType(Cpp::GetNamed("tuple_two")).data}); EXPECT_TRUE(tuple_tuple); auto tuple_tuple_callable = Cpp::MakeFunctionCallable(tuple_tuple); @@ -2143,11 +2322,11 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetFunctionCallWrapper) { } )"); - Cpp::TCppScope_t bar = + Cpp::DeclRef bar = Cpp::GetNamed("bar", Cpp::GetScope("EnumFunctionSameName")); EXPECT_TRUE(bar); - auto bar_callable = Cpp::MakeFunctionCallable(bar); + auto bar_callable = Cpp::MakeFunctionCallable(Cpp::FuncRef{bar.data}); EXPECT_EQ(bar_callable.getKind(), Cpp::JitCall::kGenericCall); Cpp::Declare(R"( @@ -2167,9 +2346,9 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetFunctionCallWrapper) { unresolved_candidate_methods); EXPECT_EQ(unresolved_candidate_methods.size(), 1); - Cpp::TCppScope_t consume = Cpp::BestOverloadFunctionMatch( + Cpp::FuncRef consume = Cpp::BestOverloadFunctionMatch( unresolved_candidate_methods, {}, - {Cpp::GetVariableType(Cpp::GetNamed("consumable"))}); + {Cpp::GetVariableType(Cpp::GetNamed("consumable")).data}); EXPECT_TRUE(consume); auto consume_callable = Cpp::MakeFunctionCallable(consume); @@ -2191,14 +2370,13 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetFunctionCallWrapper) { }; )"); - Cpp::TCppScope_t KlassProduct = Cpp::GetNamed("KlassProduct"); + Cpp::DeclRef KlassProduct = Cpp::GetNamed("KlassProduct"); EXPECT_TRUE(KlassProduct); - Cpp::TCppScope_t KlassProduct_int = - Cpp::InstantiateTemplate(KlassProduct, {TAI}); + Cpp::DeclRef KlassProduct_int = Cpp::InstantiateTemplate(KlassProduct, {TAI}); EXPECT_TRUE(KlassProduct_int); - TAI = Cpp::TemplateArgInfo(Cpp::GetType("float")); - Cpp::TCppScope_t KlassProduct_float = + TAI = Cpp::TemplateArgInfo(Cpp::GetType("float").data); + Cpp::DeclRef KlassProduct_float = Cpp::InstantiateTemplate(KlassProduct, {TAI}); EXPECT_TRUE(KlassProduct_float); @@ -2206,11 +2384,11 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetFunctionCallWrapper) { Cpp::GetOperator(KlassProduct_int, Cpp::Operator::OP_Star, operators); EXPECT_EQ(operators.size(), 2); - op = Cpp::BestOverloadFunctionMatch( - operators, {}, {{Cpp::GetTypeFromScope(KlassProduct_float)}}); - EXPECT_TRUE(op); + Cpp::FuncRef op2 = Cpp::BestOverloadFunctionMatch( + operators, {}, {{Cpp::GetTypeFromScope(KlassProduct_float).data}}); + EXPECT_TRUE(op2); - auto op_callable = Cpp::MakeFunctionCallable(op); + auto op_callable = Cpp::MakeFunctionCallable(op2); EXPECT_EQ(op_callable.getKind(), Cpp::JitCall::kGenericCall); Cpp::Declare(R"( @@ -2225,28 +2403,30 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetFunctionCallWrapper) { } )"); - Cpp::TCppScope_t TemplatedEnum = Cpp::GetScope("TemplatedEnum"); + Cpp::DeclRef TemplatedEnum = Cpp::GetScope("TemplatedEnum"); EXPECT_TRUE(TemplatedEnum); - auto TAI_enum = - Cpp::TemplateArgInfo(Cpp::GetTypeFromScope(Cpp::GetNamed("MyEnum")), "1"); - Cpp::TCppScope_t TemplatedEnum_instantiated = + auto TAI_enum = Cpp::TemplateArgInfo( + Cpp::GetTypeFromScope(Cpp::GetNamed("MyEnum")).data, "1"); + Cpp::DeclRef TemplatedEnum_instantiated = Cpp::InstantiateTemplate(TemplatedEnum, {TAI_enum}); EXPECT_TRUE(TemplatedEnum_instantiated); - Cpp::TCppObject_t obj = Cpp::Construct(TemplatedEnum_instantiated); + Cpp::ObjectRef obj = Cpp::Construct(TemplatedEnum_instantiated); EXPECT_TRUE(obj); Cpp::Destruct(obj, TemplatedEnum_instantiated); obj = nullptr; - Cpp::TCppScope_t MyNameSpace_TemplatedEnum = + Cpp::DeclRef MyNameSpace_TemplatedEnum = Cpp::GetScope("TemplatedEnum", Cpp::GetScope("MyNameSpace")); EXPECT_TRUE(TemplatedEnum); - TAI_enum = Cpp::TemplateArgInfo(Cpp::GetTypeFromScope(Cpp::GetNamed( - "MyEnum", Cpp::GetScope("MyNameSpace"))), - "1"); - Cpp::TCppScope_t MyNameSpace_TemplatedEnum_instantiated = + TAI_enum = Cpp::TemplateArgInfo( + Cpp::GetTypeFromScope( + Cpp::GetNamed("MyEnum", Cpp::GetScope("MyNameSpace"))) + .data, + "1"); + Cpp::DeclRef MyNameSpace_TemplatedEnum_instantiated = Cpp::InstantiateTemplate(MyNameSpace_TemplatedEnum, {TAI_enum}); EXPECT_TRUE(TemplatedEnum_instantiated); @@ -2259,14 +2439,103 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetFunctionCallWrapper) { auto get_fn(int x) { return [x](int y){ return x + y; }; } )"); - Cpp::TCppScope_t get_fn = Cpp::GetNamed("get_fn"); + Cpp::DeclRef get_fn = Cpp::GetNamed("get_fn"); EXPECT_TRUE(get_fn); - auto get_fn_callable = Cpp::MakeFunctionCallable(get_fn); + auto get_fn_callable = Cpp::MakeFunctionCallable(Cpp::FuncRef{get_fn.data}); EXPECT_EQ(get_fn_callable.getKind(), Cpp::JitCall::kGenericCall); - EXPECT_TRUE(Cpp::IsLambdaClass(Cpp::GetFunctionReturnType(get_fn))); - EXPECT_FALSE(Cpp::IsLambdaClass(Cpp::GetFunctionReturnType(bar))); + EXPECT_TRUE(Cpp::IsLambdaClass( + Cpp::GetFunctionReturnType(Cpp::FuncRef{get_fn.data}))); + EXPECT_FALSE( + Cpp::IsLambdaClass(Cpp::GetFunctionReturnType(Cpp::FuncRef{bar.data}))); + + Cpp::Declare(R"( + template + void callback(F&&) {} + + int (*fn_ptr)(int, int) = nullptr; + )"); + + unresolved_candidate_methods.clear(); + Cpp::GetClassTemplatedMethods("callback", Cpp::GetGlobalScope(), + unresolved_candidate_methods); + EXPECT_EQ(unresolved_candidate_methods.size(), 1); + + Cpp::FuncRef callback_func = Cpp::BestOverloadFunctionMatch( + unresolved_candidate_methods, {}, + {Cpp::GetVariableType(Cpp::GetNamed("fn_ptr")).data}); + EXPECT_TRUE(callback_func); + + auto callback_callable = Cpp::MakeFunctionCallable(callback_func); + EXPECT_EQ(callback_callable.getKind(), Cpp::JitCall::kGenericCall); + + Decls.clear(); + GetAllTopLevelDecls(R"( + bool callme(bool (f)()) { return f(); } + bool callme_ptr(bool (*f)()) { return f(); } + bool callme_ref(bool (&f)()) { return f(); } + )", + Decls, false, interpreter_args); + + auto f1 = Cpp::MakeFunctionCallable(Cpp::FuncRef{Decls[0]}); + EXPECT_EQ(f1.getKind(), Cpp::JitCall::Kind::kGenericCall); + + auto f2 = Cpp::MakeFunctionCallable(Cpp::FuncRef{Decls[1]}); + EXPECT_EQ(f2.getKind(), Cpp::JitCall::Kind::kGenericCall); + + auto f3 = Cpp::MakeFunctionCallable(Cpp::FuncRef{Decls[2]}); + EXPECT_EQ(f3.getKind(), Cpp::JitCall::Kind::kGenericCall); +} + +TYPED_TEST(CPPINTEROP_TEST_MODE, + FunctionReflection_WrapAliasTemplateReturnType) { + // Regression test for cppyy issue + // https://github.com/compiler-research/cppyy/issues/218 (original reproducer: + // `std::make_any<...>`, return type `std::enable_if_t, + // std::any>`). Building a wrapper for a function template whose return type is + // a type-alias-template specialisation used to fail to compile; the snippet + // below is a stdlib-free distillation. It needs three ingredients: an alias + // (`enable_if_t`) whose sugar carries a non-type argument that prints as an + // *expression* (`trait_v`, which FullyQualifiedName does not qualify); a + // predicate in a namespace, so unqualified `trait_v` does not resolve in the + // global-scope wrapper; and a class (non-builtin) canonical type, so + // get_type_as_string keeps the sugar instead of taking its isBuiltinType() + // branch. See get_type_as_string for how the fix desugars this. + // + // Only checks JitCall::getKind(), i.e. that the wrapper *compiles*; it never + // Invoke()s it, so no out-of-process skip is needed. +#ifdef EMSCRIPTEN +#if CLANG_VERSION_MAJOR > 21 + // The Emscripten JIT (LLVM 22) cannot compile this by-value wrapper -- a + // separate limitation from the type printing under test, shared with the + // other placement-new wrapper tests (e.g. FunctionReflection_Construct). + GTEST_SKIP() << "Test fails for Emscripten builds using LLVM 22"; +#endif +#endif + std::vector interpreter_args = {"-std=c++17", "-include", "new"}; + TestFixture::CreateInterpreter(interpreter_args); + // Single namespace block: a preceding top-level class plus a namespace in one + // process() call makes the LLVM 20 REPL wrap the namespace into non-global + // scope. + Interp->process(R"( + namespace N { + struct Ret { int x; }; + template inline constexpr bool trait_v = sizeof(T) > 0; + template struct ei {}; + template struct ei { using type = T; }; + template using enable_if_t = typename ei::type; + template enable_if_t, Ret> f() { return {}; } + } + )"); + + auto spec = Cpp::InstantiateTemplateFunctionFromString("N::f"); + ASSERT_TRUE(spec) << "Sema failed to substitute N::f"; + + // Without the fix the wrapper fails to compile (`use of undeclared identifier + // 'trait_v'`) and MakeFunctionCallable returns a kUnknown JitCall. + Cpp::JitCall JC = Cpp::MakeFunctionCallable(spec); + EXPECT_EQ(JC.getKind(), Cpp::JitCall::kGenericCall); } TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_IsConstMethod) { @@ -2280,7 +2549,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_IsConstMethod) { GetAllTopLevelDecls(code, Decls); GetAllSubDecls(Decls[0], SubDecls); - Cpp::TCppFunction_t method = nullptr; // Simulate an invalid method pointer + Cpp::FuncRef method = nullptr; // Simulate an invalid method pointer EXPECT_TRUE(Cpp::IsConstMethod(SubDecls[1])); // f1 EXPECT_FALSE(Cpp::IsConstMethod(SubDecls[2])); // f2 @@ -2378,15 +2647,13 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetFunctionArgDefault) { ASTContext& C = Interp->getCI()->getASTContext(); std::vector template_args = {C.IntTy.getAsOpaquePtr()}; - Cpp::TCppScope_t my_struct = - Cpp::InstantiateTemplate(Decls[6], template_args); + Cpp::DeclRef my_struct = Cpp::InstantiateTemplate(Decls[6], template_args); EXPECT_TRUE(my_struct); - std::vector fns = - Cpp::GetFunctionsUsingName(my_struct, "fn"); + std::vector fns = Cpp::GetFunctionsUsingName(my_struct, "fn"); EXPECT_EQ(fns.size(), 1); - Cpp::TCppScope_t fn = fns[0]; + Cpp::FuncRef fn = fns[0]; EXPECT_EQ(Cpp::GetFunctionArgDefault(fn, 0), ""); EXPECT_EQ(Cpp::GetFunctionArgDefault(fn, 1), "S()"); } @@ -2422,9 +2689,9 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_Construct) { GetAllTopLevelDecls(code, Decls, false, interpreter_args); GetAllSubDecls(Decls[1], SubDecls); testing::internal::CaptureStdout(); - Cpp::TCppScope_t scope = Cpp::GetNamed("C"); - Cpp::TCppObject_t object = Cpp::Construct(scope); - EXPECT_TRUE(object != nullptr); + Cpp::DeclRef scope = Cpp::GetNamed("C"); + Cpp::ObjectRef object = Cpp::Construct(scope); + EXPECT_TRUE(object); std::string output = testing::internal::GetCapturedStdout(); EXPECT_EQ(output, "Constructor Executed"); output.clear(); @@ -2433,8 +2700,8 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_Construct) { // Placement. testing::internal::CaptureStdout(); - void* where = Cpp::Allocate(scope); - EXPECT_TRUE(where == Cpp::Construct(scope, where)); + void* where = Cpp::Allocate(scope).data; + EXPECT_TRUE(where == Cpp::Construct(scope, where).data); // Check for the value of x which should be at the start of the object. EXPECT_TRUE(*(int*)where == 12345); output = testing::internal::GetCapturedStdout(); @@ -2445,8 +2712,8 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_Construct) { // Pass a constructor testing::internal::CaptureStdout(); - where = Cpp::Allocate(scope); - EXPECT_TRUE(where == Cpp::Construct(SubDecls[3], where)); + where = Cpp::Allocate(scope).data; + EXPECT_TRUE(where == Cpp::Construct(SubDecls[3], where).data); EXPECT_TRUE(*(int*)where == 12345); output = testing::internal::GetCapturedStdout(); EXPECT_EQ(output, "Constructor Executed"); @@ -2459,9 +2726,9 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_Construct) { // `where` stays alive for Deallocate. FIXME: Construct's failure path // could own the arena release itself rather than leaking this contract // to every caller — see Cpp::Construct in lib/CppInterOp/CppInterOp.cpp. - where = Cpp::Allocate(scope); - void* construct_fail = Cpp::Construct(Decls[2], where); - EXPECT_TRUE(construct_fail == nullptr); + where = Cpp::Allocate(scope).data; + auto construct_fail = Cpp::Construct(Decls[2], where); + EXPECT_FALSE(construct_fail); Cpp::Deallocate(scope, where); } @@ -2491,12 +2758,12 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_ConstructPOD) { }; })"); - auto* ns = Cpp::GetNamed("PODS"); - Cpp::TCppScope_t scope = Cpp::GetNamed("SomePOD_B", ns); + auto ns = Cpp::GetNamed("PODS"); + Cpp::DeclRef scope = Cpp::GetNamed("SomePOD_B", ns); EXPECT_TRUE(scope); - Cpp::TCppObject_t object = Cpp::Construct(scope); - EXPECT_TRUE(object != nullptr); - int* fInt = reinterpret_cast(reinterpret_cast(object)); + Cpp::ObjectRef object = Cpp::Construct(scope); + EXPECT_TRUE(object); + int* fInt = reinterpret_cast(reinterpret_cast(object.data)); EXPECT_TRUE(*fInt == 0); Cpp::Destruct(object, scope, /*withFree=*/true, /*count=*/0); @@ -2504,8 +2771,8 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_ConstructPOD) { EXPECT_TRUE(scope); object = Cpp::Construct(scope); EXPECT_TRUE(object); - auto* fDouble = - reinterpret_cast(reinterpret_cast(object) + sizeof(int)); + auto* fDouble = reinterpret_cast( + reinterpret_cast(object.data) + sizeof(int)); EXPECT_EQ(*fDouble, 0.0); Cpp::Destruct(object, scope, /*withFree=*/true, /*count=*/0); } @@ -2548,10 +2815,10 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_ConstructNested) { )"); testing::internal::CaptureStdout(); - Cpp::TCppScope_t scope_A = Cpp::GetNamed("A"); - Cpp::TCppScope_t scope_B = Cpp::GetNamed("B"); - Cpp::TCppObject_t object = Cpp::Construct(scope_B); - EXPECT_TRUE(object != nullptr); + Cpp::DeclRef scope_A = Cpp::GetNamed("A"); + Cpp::DeclRef scope_B = Cpp::GetNamed("B"); + Cpp::ObjectRef object = Cpp::Construct(scope_B); + EXPECT_TRUE(object); std::string output = testing::internal::GetCapturedStdout(); EXPECT_EQ(output, "A Constructor Called\nB Constructor Called\n"); output.clear(); @@ -2559,8 +2826,8 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_ConstructNested) { // In-memory construction testing::internal::CaptureStdout(); - void* arena = Cpp::Allocate(scope_B); - EXPECT_TRUE(arena == Cpp::Construct(scope_B, arena)); + void* arena = Cpp::Allocate(scope_B).data; + EXPECT_TRUE(arena == Cpp::Construct(scope_B, arena).data); // Check if both integers a_val and b_val were set. EXPECT_EQ(*(int*)arena, 7); @@ -2598,14 +2865,14 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_ConstructArray) { }; )"); - Cpp::TCppScope_t scope = Cpp::GetNamed("C"); + Cpp::DeclRef scope = Cpp::GetNamed("C"); std::string output; size_t a = 5; // Construct an array of 5 objects - void* where = Cpp::Allocate(scope, a); // operator new + void* where = Cpp::Allocate(scope, a).data; // operator new testing::internal::CaptureStdout(); - EXPECT_TRUE(where == Cpp::Construct(scope, where, a)); // placement new + EXPECT_TRUE(where == Cpp::Construct(scope, where, a).data); // placement new // Check for the value of x which should be at the start of the object. EXPECT_TRUE(*(int*)where == 42); // Check for the value of x in the second object @@ -2652,8 +2919,8 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_Destruct) { )"); testing::internal::CaptureStdout(); - Cpp::TCppScope_t scope = Cpp::GetNamed("C"); - Cpp::TCppObject_t object = Cpp::Construct(scope); + Cpp::DeclRef scope = Cpp::GetNamed("C"); + Cpp::ObjectRef object = Cpp::Construct(scope); EXPECT_TRUE(Cpp::Destruct(object, scope)); std::string output = testing::internal::GetCapturedStdout(); @@ -2717,12 +2984,12 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_DestructArray) { }; )"); - Cpp::TCppScope_t scope = Cpp::GetNamed("C"); + Cpp::DeclRef scope = Cpp::GetNamed("C"); std::string output; size_t a = 5; // Construct an array of 5 objects - void* where = Cpp::Allocate(scope, a); // operator new - EXPECT_TRUE(where == Cpp::Construct(scope, where, a)); // placement new + void* where = Cpp::Allocate(scope, a).data; // operator new + EXPECT_TRUE(where == Cpp::Construct(scope, where, a).data); // placement new // verify the array of objects has been constructed int* obj = reinterpret_cast(reinterpret_cast(where) + @@ -2754,7 +3021,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_DestructArray) { // perform the same withFree=true where = nullptr; - where = Cpp::Construct(scope, nullptr, a); + where = Cpp::Construct(scope, nullptr, a).data; EXPECT_TRUE(where); testing::internal::CaptureStdout(); EXPECT_TRUE(Cpp::Destruct(where, scope, true, a)); @@ -2815,17 +3082,17 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_FailingTest1) { bool is_equal(const C1& c1, const C2& c2) { return (bool)(c1 == c2); } )")); - Cpp::TCppType_t o1 = Cpp::GetTypeFromScope(Cpp::GetNamed("o1")); - Cpp::TCppType_t o2 = Cpp::GetTypeFromScope(Cpp::GetNamed("o2")); - std::vector fns; + Cpp::TypeRef o1 = Cpp::GetTypeFromScope(Cpp::GetNamed("o1")); + Cpp::TypeRef o2 = Cpp::GetTypeFromScope(Cpp::GetNamed("o2")); + std::vector fns; Cpp::GetClassTemplatedMethods("is_equal", Cpp::GetGlobalScope(), fns); EXPECT_EQ(fns.size(), 1); - std::vector args = {{o1}, {o2}}; - Cpp::TCppScope_t fn = Cpp::InstantiateTemplate(fns[0], args); + std::vector args = {{o1.data}, {o2.data}}; + Cpp::DeclRef fn = Cpp::InstantiateTemplate(Cpp::DeclRef{fns[0].data}, args); EXPECT_TRUE(fn); - Cpp::JitCall jit_call = Cpp::MakeFunctionCallable(fn); + Cpp::JitCall jit_call = Cpp::MakeFunctionCallable(Cpp::FuncRef{fn.data}); EXPECT_EQ(jit_call.getKind(), Cpp::JitCall::kUnknown); // expected to fail EXPECT_FALSE(Cpp::Declare("int x = 1;")); EXPECT_FALSE(Cpp::Declare("int y = x;")); @@ -2965,3 +3232,514 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_IsExplicitDeductionGuide) { EXPECT_EQ(Cpp::GetTypeAsString(Cpp::GetFunctionReturnType(guide)), "Wrapper"); } + +// C++23 "deducing this" (explicit object parameters, P0847R7): the object +// parameter binds to the receiver, not the argument list. +TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_DeducingThisIntrospection) { + std::vector Decls; + std::vector SubDecls; + std::string code = R"( + struct Widget { + int value = 42; + int get(this Widget& self) { return self.value; } + int add(this Widget& self, int x, int y = 5) { return self.value + x + y; } + int plain(int x) { return value + x; } + }; + )"; + + GetAllTopLevelDecls(code, Decls, /*filter_implicitGenerated=*/true, + /*interpreter_args=*/{"-std=c++23"}); + GetAllSubDecls(Decls[0], SubDecls, /*filter_implicitGenerated=*/true); + + // SubDecls: [0]=field value, [1]=get, [2]=add, [3]=plain + Decl* get = SubDecls[1]; + Decl* add = SubDecls[2]; + Decl* plain = SubDecls[3]; + + // The explicit object parameter is not counted as a callee argument. + EXPECT_EQ(Cpp::GetFunctionNumArgs(get), (size_t)0); + EXPECT_EQ(Cpp::GetFunctionRequiredArgs(get), (size_t)0); + + EXPECT_EQ(Cpp::GetFunctionNumArgs(add), (size_t)2); + EXPECT_EQ(Cpp::GetFunctionRequiredArgs(add), (size_t)1); // y defaulted + + // The non-object parameters are exposed at 0-based indices that skip `self`. + EXPECT_EQ(Cpp::GetTypeAsString(Cpp::GetFunctionArgType(add, 0)), "int"); + EXPECT_EQ(Cpp::GetTypeAsString(Cpp::GetFunctionArgType(add, 1)), "int"); + EXPECT_EQ(Cpp::GetFunctionArgName(add, 0), "x"); + EXPECT_EQ(Cpp::GetFunctionArgName(add, 1), "y"); + EXPECT_EQ(Cpp::GetFunctionArgDefault(add, 1), "5"); + + // A traditional (implicit-object) method is unaffected. + EXPECT_EQ(Cpp::GetFunctionNumArgs(plain), (size_t)1); + EXPECT_EQ(Cpp::GetFunctionArgName(plain, 0), "x"); +} + +TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_DeducingThisJitCall) { +#ifdef EMSCRIPTEN +#if CLANG_VERSION_MAJOR > 21 + GTEST_SKIP() << "Test fails for Emscripten builds using LLVM 22"; +#endif +#endif + if (TypeParam::isOutOfProcess) + GTEST_SKIP() << "Test fails for OOP JIT builds"; + + std::string code = R"( + struct Widget { + int value = 42; + int get(this Widget& self) { return self.value; } + int add(this Widget& self, int x, int y) { return self.value + x + y; } + }; + )"; + + std::vector Decls; + // `-include new` is needed for the constructor wrapper's placement new. + GetAllTopLevelDecls(code, Decls, /*filter_implicitGenerated=*/false, + /*interpreter_args=*/{"-std=c++23", "-include", "new"}); + + Cpp::DeclRef Widget = Cpp::GetNamed("Widget"); + ASSERT_TRUE(Widget); + + // Construct a Widget so the in-class initializer (value = 42) runs. + auto Ctor = Cpp::MakeFunctionCallable(Cpp::GetDefaultConstructor(Widget)); + void* object = nullptr; + Ctor.Invoke((void*)&object, {}, /*self=*/nullptr); + ASSERT_TRUE(object); + + // get(): explicit object parameter bound to the receiver, no call args. + Cpp::JitCall GetCall = Cpp::MakeFunctionCallable( + Cpp::FuncRef{Cpp::GetNamed("get", Widget).data}); + EXPECT_EQ(GetCall.getKind(), Cpp::JitCall::kGenericCall); + int result = 0; + GetCall.Invoke(&result, {}, object); + EXPECT_EQ(result, 42); + + // add(): explicit object parameter plus regular arguments. + Cpp::JitCall AddCall = Cpp::MakeFunctionCallable( + Cpp::FuncRef{Cpp::GetNamed("add", Widget).data}); + int x = 20; + int y = 3; + std::array args = {(void*)&x, (void*)&y}; + result = 0; + AddCall.Invoke(&result, {args.data(), /*args_size=*/2}, object); + EXPECT_EQ(result, 42 + 20 + 3); + + Cpp::Destruct(object, Widget); +} + +TYPED_TEST(CPPINTEROP_TEST_MODE, + FunctionReflection_DeducingThisRValueRefJitCall) { +#ifdef EMSCRIPTEN +#if CLANG_VERSION_MAJOR > 21 + GTEST_SKIP() << "Test fails for Emscripten builds using LLVM 22"; +#endif +#endif + if (TypeParam::isOutOfProcess) + GTEST_SKIP() << "Test fails for OOP JIT builds"; + + // The wrapper must bind the receiver as an rvalue for both the C++23 + // explicit-object form (`this W&&`) and the traditional `&&` qualifier. + std::string code = R"( + struct RWidget { + int value = 13; + int consume(this RWidget&& self) { return self.value; } + }; + struct QWidget { + int value = 17; + int consume() && { return value; } + }; + )"; + + std::vector Decls; + GetAllTopLevelDecls(code, Decls, /*filter_implicitGenerated=*/false, + /*interpreter_args=*/{"-std=c++23", "-include", "new"}); + + for (const char* name : {"RWidget", "QWidget"}) { + Cpp::DeclRef W = Cpp::GetNamed(name); + ASSERT_TRUE(W) << name; + auto Ctor = Cpp::MakeFunctionCallable(Cpp::GetDefaultConstructor(W)); + void* object = nullptr; + Ctor.Invoke((void*)&object); + ASSERT_TRUE(object) << name; + + Cpp::JitCall Call = Cpp::MakeFunctionCallable( + Cpp::FuncRef{Cpp::GetNamed("consume", W).data}); + EXPECT_EQ(Call.getKind(), Cpp::JitCall::kGenericCall) << name; + int result = 0; + Call.Invoke(&result, {}, object); + EXPECT_EQ(result, std::string(name) == "RWidget" ? 13 : 17) << name; + + Cpp::Destruct(object, W); + } +} + +TYPED_TEST(CPPINTEROP_TEST_MODE, + FunctionReflection_DeducingThisTemplateOverloadMatch) { + std::vector Decls; + std::vector SubDecls; + std::string code = R"( + struct TWidget { + int value = 9; + template int via(this Self&& self) { return self.value; } + }; + )"; + + GetAllTopLevelDecls(code, Decls, /*filter_implicitGenerated=*/true, + /*interpreter_args=*/{"-std=c++23"}); + GetAllSubDecls(Decls[0], SubDecls, /*filter_implicitGenerated=*/true); + + std::vector candidates; + for (auto* decl : SubDecls) + if (Cpp::IsTemplatedFunction(decl)) + candidates.push_back((Cpp::FuncRef)decl); + ASSERT_EQ(candidates.size(), (size_t)1); + + // No explicit template/call args: `Self` deduces from the synthesized + // receiver (only the AddMethodTemplateCandidate path can do this). + std::vector no_explicit_args; + std::vector no_args; + Cpp::FuncRef matched = + Cpp::BestOverloadFunctionMatch(candidates, no_explicit_args, no_args); + ASSERT_TRUE(matched); + EXPECT_NE(Cpp::GetFunctionSignature(matched).find("via"), std::string::npos); +} + +TYPED_TEST(CPPINTEROP_TEST_MODE, + FunctionReflection_DeducingThisAbbreviatedAuto) { + // `this auto&&` is sugar for an invented template parameter: a method + // template with a distinct AST shape from an explicit `template`. + // It must still deduce the object parameter from the synthesized receiver. + std::vector Decls; + std::vector SubDecls; + std::string code = R"( + struct AAWidget { + int value = 23; + int get(this auto&& self) { return self.value; } + }; + )"; + + GetAllTopLevelDecls(code, Decls, /*filter_implicitGenerated=*/true, + /*interpreter_args=*/{"-std=c++23"}); + GetAllSubDecls(Decls[0], SubDecls, /*filter_implicitGenerated=*/true); + + std::vector candidates; + for (auto* decl : SubDecls) + if (Cpp::IsTemplatedFunction(decl)) + candidates.push_back((Cpp::FuncRef)decl); + ASSERT_EQ(candidates.size(), (size_t)1); + + std::vector no_explicit_args; + std::vector no_args; + Cpp::FuncRef matched = + Cpp::BestOverloadFunctionMatch(candidates, no_explicit_args, no_args); + ASSERT_TRUE(matched); + EXPECT_NE(Cpp::GetFunctionSignature(matched).find("get"), std::string::npos); +} + +TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_DeducingThisByValueCopy) { +#ifdef EMSCRIPTEN +#if CLANG_VERSION_MAJOR > 21 + GTEST_SKIP() << "Test fails for Emscripten builds using LLVM 22"; +#endif +#endif + if (TypeParam::isOutOfProcess) + GTEST_SKIP() << "Test fails for OOP JIT builds"; + + // A by-value explicit object parameter (`this W self`) operates on an + // independent copy: mutating it must not affect the original receiver. + std::string code = R"( + struct CopyWidget { + int value = 1; + int bump(this CopyWidget self) { self.value += 100; return self.value; } + int read(this CopyWidget& self) { return self.value; } + }; + )"; + + std::vector Decls; + GetAllTopLevelDecls(code, Decls, /*filter_implicitGenerated=*/false, + /*interpreter_args=*/{"-std=c++23", "-include", "new"}); + + Cpp::DeclRef W = Cpp::GetNamed("CopyWidget"); + ASSERT_TRUE(W); + auto Ctor = Cpp::MakeFunctionCallable(Cpp::GetDefaultConstructor(W)); + void* object = nullptr; + Ctor.Invoke((void*)&object, {}, /*self=*/nullptr); + ASSERT_TRUE(object); + + Cpp::JitCall Bump = + Cpp::MakeFunctionCallable(Cpp::FuncRef{Cpp::GetNamed("bump", W).data}); + int result = 0; + Bump.Invoke(&result, {}, object); + EXPECT_EQ(result, 101); // the copy was mutated + + Cpp::JitCall Read = + Cpp::MakeFunctionCallable(Cpp::FuncRef{Cpp::GetNamed("read", W).data}); + result = 0; + Read.Invoke(&result, {}, object); + EXPECT_EQ(result, 1); // ... the original is untouched + + Cpp::Destruct(object, W); +} + +TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_DeducingThisInheritance) { +#ifdef EMSCRIPTEN +#if CLANG_VERSION_MAJOR > 21 + GTEST_SKIP() << "Test fails for Emscripten builds using LLVM 22"; +#endif +#endif + if (TypeParam::isOutOfProcess) + GTEST_SKIP() << "Test fails for OOP JIT builds"; + + // A base-class explicit object method invoked on a derived object: the + // wrapper binds a Derived* receiver to a Base& object parameter. + std::string code = R"( + struct Base { + int value = 8; + int get(this Base& self) { return self.value; } + }; + struct Derived : Base { }; + )"; + + std::vector Decls; + GetAllTopLevelDecls(code, Decls, /*filter_implicitGenerated=*/false, + /*interpreter_args=*/{"-std=c++23", "-include", "new"}); + + Cpp::DeclRef Derived = Cpp::GetNamed("Derived"); + Cpp::DeclRef Base = Cpp::GetNamed("Base"); + ASSERT_TRUE(Derived); + ASSERT_TRUE(Base); + auto Ctor = Cpp::MakeFunctionCallable(Cpp::GetDefaultConstructor(Derived)); + void* object = nullptr; + Ctor.Invoke((void*)&object, {}, /*self=*/nullptr); + ASSERT_TRUE(object); + + // get() is declared on Base, invoked with the Derived object. + Cpp::JitCall Get = + Cpp::MakeFunctionCallable(Cpp::FuncRef{Cpp::GetNamed("get", Base).data}); + EXPECT_EQ(Get.getKind(), Cpp::JitCall::kGenericCall); + int result = 0; + Get.Invoke(&result, {}, object); + EXPECT_EQ(result, 8); + + Cpp::Destruct(object, Derived); +} + +// Use-cases from https://devblogs.microsoft.com/cppblog/cpp23-deducing-this/ +// Cases that live inside a function body are driven via a JIT-called helper. + +// JIT-call a nullary `int ns::fn()`. (GetNamed takes a name + scope, so the +// namespace is resolved first.) +static int JitCallIntNullary(const char* ns, const char* fn) { + Cpp::DeclRef Scope = Cpp::GetNamed(ns); + EXPECT_TRUE(Scope) << ns; + Cpp::DeclRef Fn = Cpp::GetNamed(fn, Scope); + EXPECT_TRUE(Fn) << fn; + Cpp::JitCall JC = Cpp::MakeFunctionCallable(Cpp::FuncRef{Fn.data}); + EXPECT_EQ(JC.getKind(), Cpp::JitCall::kGenericCall) << fn; + int result = 0; + JC.Invoke(&result, {}); + return result; +} + +TYPED_TEST(CPPINTEROP_TEST_MODE, + FunctionReflection_DeducingThisBlogDeduplication) { +#ifdef EMSCRIPTEN +#if CLANG_VERSION_MAJOR > 21 + GTEST_SKIP() << "Test fails for Emscripten builds using LLVM 22"; +#endif +#endif + if (TypeParam::isOutOfProcess) + GTEST_SKIP() << "Test fails for OOP JIT builds"; + + // Blog use-case 1: code de-duplication. One forwarding accessor + // `value(this Self&&)` replaces the cv/ref overloads; the driver writes + // through it on an lvalue and reads the mutation back. + std::string code = R"( + #include + namespace BlogDedup { + struct Optional { + int m_value = 5; + template auto&& value(this Self&& self) { + return std::forward(self).m_value; + } + }; + int drive() { Optional o; o.value() = 17; return o.value(); } + } + )"; + + std::vector Decls; + GetAllTopLevelDecls(code, Decls, /*filter_implicitGenerated=*/false, + /*interpreter_args=*/{"-std=c++23", "-include", "new"}); + EXPECT_EQ(JitCallIntNullary("BlogDedup", "drive"), 17); +} + +TYPED_TEST(CPPINTEROP_TEST_MODE, + FunctionReflection_DeducingThisBlogCRTPPostfix) { +#ifdef EMSCRIPTEN +#if CLANG_VERSION_MAJOR > 21 + GTEST_SKIP() << "Test fails for Emscripten builds using LLVM 22"; +#endif +#endif + if (TypeParam::isOutOfProcess) + GTEST_SKIP() << "Test fails for OOP JIT builds"; + + // Blog use-case 2: CRTP without templating the base. `add_postfix_increment` + // supplies `operator++(this Self&&, int)` once; the derived prefix operator + // hides it, so a using-declaration re-exposes it (standard name hiding). + std::string code = R"( + namespace BlogCRTP { + struct add_postfix_increment { + template + auto operator++(this Self&& self, int) { auto tmp = self; ++self; return tmp; } + }; + struct some_type : add_postfix_increment { + using add_postfix_increment::operator++; + int v = 0; + some_type& operator++() { ++v; return *this; } + }; + int drive() { some_type c; auto old = c++; return old.v * 100 + c.v; } + } + )"; + + std::vector Decls; + GetAllTopLevelDecls(code, Decls, /*filter_implicitGenerated=*/false, + /*interpreter_args=*/{"-std=c++23", "-include", "new"}); + EXPECT_EQ(JitCallIntNullary("BlogCRTP", "drive"), 1); // old.v=0, c.v=1 +} + +TYPED_TEST(CPPINTEROP_TEST_MODE, + FunctionReflection_DeducingThisBlogRecursiveLambda) { +#ifdef EMSCRIPTEN +#if CLANG_VERSION_MAJOR > 21 + GTEST_SKIP() << "Test fails for Emscripten builds using LLVM 22"; +#endif +#endif + if (TypeParam::isOutOfProcess) + GTEST_SKIP() << "Test fails for OOP JIT builds"; + + // Blog use-case 4: recursive lambdas via the explicit object parameter. + std::string code = R"( + namespace BlogRecLambda { + int fib(int n) { + auto f = [](this auto const& self, int n) -> int { + return n < 2 ? n : self(n - 1) + self(n - 2); + }; + return f(n); + } + int drive() { return fib(10); } + } + )"; + + std::vector Decls; + GetAllTopLevelDecls(code, Decls, /*filter_implicitGenerated=*/false, + /*interpreter_args=*/{"-std=c++23", "-include", "new"}); + EXPECT_EQ(JitCallIntNullary("BlogRecLambda", "drive"), 55); +} + +TYPED_TEST(CPPINTEROP_TEST_MODE, + FunctionReflection_DeducingThisBlogLambdaForwarding) { +#ifdef EMSCRIPTEN +#if CLANG_VERSION_MAJOR > 21 + GTEST_SKIP() << "Test fails for Emscripten builds using LLVM 22"; +#endif +#endif + if (TypeParam::isOutOfProcess) + GTEST_SKIP() << "Test fails for OOP JIT builds"; + + // Blog use-case 3: a closure with an explicit object parameter forwards based + // on its own value category. (std::forward_like is not in the host libstdc++; + // the deducing-this closure mechanism is what is exercised.) + std::string code = R"( + #include + namespace BlogLambdaFwd { + struct Scheduler { int submit(int m) { return m; } }; + int drive() { + Scheduler scheduler; + int message = 42; + auto callback = [message, &scheduler](this auto&& self) -> int { + return scheduler.submit(message); + }; + return callback(); + } + } + )"; + + std::vector Decls; + GetAllTopLevelDecls(code, Decls, /*filter_implicitGenerated=*/false, + /*interpreter_args=*/{"-std=c++23", "-include", "new"}); + EXPECT_EQ(JitCallIntNullary("BlogLambdaFwd", "drive"), 42); +} + +TYPED_TEST(CPPINTEROP_TEST_MODE, + FunctionReflection_DeducingThisBlogPassByValue) { +#ifdef EMSCRIPTEN +#if CLANG_VERSION_MAJOR > 21 + GTEST_SKIP() << "Test fails for Emscripten builds using LLVM 22"; +#endif +#endif + if (TypeParam::isOutOfProcess) + GTEST_SKIP() << "Test fails for OOP JIT builds"; + + // Blog use-case 5: pass the object by value (good codegen for small types). + // Invoke the explicit-object method directly on a constructed object. + std::string code = R"( + namespace BlogByValue { + struct just_a_little_guy { + int how_smol = 21; + int uwu(this just_a_little_guy self) { return self.how_smol * 2; } + }; + } + )"; + + std::vector Decls; + GetAllTopLevelDecls(code, Decls, /*filter_implicitGenerated=*/false, + /*interpreter_args=*/{"-std=c++23", "-include", "new"}); + + Cpp::DeclRef W = + Cpp::GetNamed("just_a_little_guy", Cpp::GetNamed("BlogByValue")); + ASSERT_TRUE(W); + auto Ctor = Cpp::MakeFunctionCallable(Cpp::GetDefaultConstructor(W)); + void* object = nullptr; + Ctor.Invoke((void*)&object, {}, /*self=*/nullptr); + ASSERT_TRUE(object); + + Cpp::JitCall Uwu = + Cpp::MakeFunctionCallable(Cpp::FuncRef{Cpp::GetNamed("uwu", W).data}); + int result = 0; + Uwu.Invoke(&result, {}, object); + EXPECT_EQ(result, 42); + + Cpp::Destruct(object, W); +} + +TYPED_TEST(CPPINTEROP_TEST_MODE, + FunctionReflection_DeducingThisBlogSfinaeTransform) { +#ifdef EMSCRIPTEN +#if CLANG_VERSION_MAJOR > 21 + GTEST_SKIP() << "Test fails for Emscripten builds using LLVM 22"; +#endif +#endif + if (TypeParam::isOutOfProcess) + GTEST_SKIP() << "Test fails for OOP JIT builds"; + + // Blog use-case 6: SFINAE-friendly callables (optional::transform). The + // cv/ref category flows through Self; a callable is applied to the contained + // value. + std::string code = R"( + namespace BlogTransform { + template + struct Optional6 { + T m_value; + template + auto transform(this Self&& self, F&& f) { return f(self.m_value); } + }; + int triple(int x) { return x * 3; } + int drive() { Optional6 o{14}; return o.transform(triple); } + } + )"; + + std::vector Decls; + GetAllTopLevelDecls(code, Decls, /*filter_implicitGenerated=*/false, + /*interpreter_args=*/{"-std=c++23", "-include", "new"}); + EXPECT_EQ(JitCallIntNullary("BlogTransform", "drive"), 42); +} diff --git a/interpreter/CppInterOp/unittests/CppInterOp/HandleTypesTest.cpp b/interpreter/CppInterOp/unittests/CppInterOp/HandleTypesTest.cpp new file mode 100644 index 0000000000000..cb3614e537452 --- /dev/null +++ b/interpreter/CppInterOp/unittests/CppInterOp/HandleTypesTest.cpp @@ -0,0 +1,135 @@ +// Tests for the opaque handle types in CppInterOpTypes.h: +// ABI invariants, type safety (const widening, cross-kind rejection), +// and wrap/unwrap round-trip. No interpreter required. + +#include "../../lib/CppInterOp/Unwrap.h" +#include "CppInterOp/CppInterOp.h" +#include "gtest/gtest.h" + +#include +#include +#include + +using namespace Cpp; + +namespace { +// SFINAE traits used to assert that an expression does NOT compile. +template +struct is_eq_comparable : std::false_type {}; +template +struct is_eq_comparable< + A, B, std::void_t() == std::declval())>> + : std::true_type {}; + +bool void_ptr_takes_nonnull(void* p) { return p != nullptr; } +} // namespace + +// -- ABI: handles must be layout-compatible with void* ------------------ + +static_assert(sizeof(DeclRef) == sizeof(void*)); +static_assert(alignof(DeclRef) == alignof(void*)); +static_assert(std::is_standard_layout_v); +static_assert(std::is_trivially_copyable_v); + +// Const and mutable variants must share ABI so a dispatch wrapper can +// reinterpret-cast between them without marshalling. +static_assert(sizeof(DeclRef) == sizeof(ConstDeclRef)); +static_assert(sizeof(TypeRef) == sizeof(ConstTypeRef)); +static_assert(sizeof(FuncRef) == sizeof(ConstFuncRef)); + +// -- Type safety: implicit conversions ---------------------------------- + +// Mutable → const widening is allowed. +static_assert(std::is_convertible_v); +static_assert(std::is_convertible_v); +static_assert(std::is_convertible_v); + +// Const → mutable narrowing is rejected. +static_assert(!std::is_convertible_v); +static_assert(!std::is_assignable_v); + +// Cross-kind conversions are rejected (Decl ≠ Type ≠ Func). +static_assert(!std::is_convertible_v); +static_assert(!std::is_convertible_v); +static_assert(!std::is_convertible_v); + +// Cross-kind equality is rejected — the entire point of distinct types. +static_assert(is_eq_comparable::value); +static_assert(!is_eq_comparable::value); +static_assert(!is_eq_comparable::value); + +// unwrap preserves const: T* for mutable handles, const T* for const. +static_assert(std::is_same_v(DeclRef{})), int*>); +static_assert( + std::is_same_v(ConstDeclRef{})), const int*>); + +// -- TemplateArgInfo: C-ABI layout cannot drift ------------------------- + +static_assert(std::is_standard_layout_v); +static_assert(sizeof(TemplateArgInfo) == 2 * sizeof(void*)); +static_assert(offsetof(TemplateArgInfo, m_Type) == 0); +static_assert(offsetof(TemplateArgInfo, m_IntegralValue) == sizeof(void*)); + +// -- Runtime -------------------------------------------------------------- + +TEST(HandleTypes, NullSemantics) { + EXPECT_FALSE(DeclRef{}); + EXPECT_FALSE(ConstDeclRef{}); + EXPECT_EQ(DeclRef{}, nullptr); + EXPECT_TRUE(DeclRef(nullptr) == nullptr); +} + +TEST(HandleTypes, WrapUnwrapRoundTrip) { + int x = 0; + DeclRef d = wrap(static_cast(&x)); + EXPECT_EQ(unwrap(d), &x); + + // Mutable → const widening is value-preserving. + ConstDeclRef cd = d; + EXPECT_EQ(unwrap(cd), &x); +} + +TEST(HandleTypes, Equality) { + int x = 0, y = 0; + DeclRef a(&x), b(&x), c(&y); + EXPECT_EQ(a, b); + EXPECT_NE(a, c); +} + +// std::hash specializations let handles key unordered containers. The +// hash must agree with std::hash on the underlying pointer and +// distinguish distinct pointers. +TEST(HandleTypes, Hash) { + int x = 0, y = 0; + DeclRef a(&x), b(&x), c(&y); + std::hash h; + EXPECT_EQ(h(a), h(b)); + EXPECT_EQ(h(a), std::hash{}(&x)); + EXPECT_NE(h(a), h(c)); + + std::unordered_set s{a, b, c}; + EXPECT_EQ(s.size(), 2u); + EXPECT_TRUE(s.count(DeclRef(&x))); + EXPECT_FALSE(s.count(DeclRef{})); +} + +// The dispatch ABI cppyy uses reinterpret-casts a dlsym'd void*-taking +// function pointer to a handle-taking one. Verify that the struct{void*} +// calling convention is byte-identical to passing void*. A union swaps +// the function-pointer type without triggering -Wcast-function-type. +TEST(HandleTypes, AbiCompatibleWithVoidPtr) { + union { + bool (*void_ptr_fn)(void*); + bool (*handle_fn)(DeclRef); + } pun{&void_ptr_takes_nonnull}; + + // The handle-taking pointer comes from dlsym at runtime, so the compiler + // cannot see through it. If it is a compile-time constant, GCC 11 exploits + // the type mismatch (UB) to constant-fold the call giving a wrong + // reslt. Route the call through a volatile local so the compiler treats + // the pointer as runtime-opaque + volatile auto handle_fn = pun.handle_fn; + int x = 0; + EXPECT_TRUE(handle_fn(DeclRef(&x))); + EXPECT_FALSE(handle_fn(DeclRef{})); +} diff --git a/interpreter/CppInterOp/unittests/CppInterOp/InterpreterTest.cpp b/interpreter/CppInterOp/unittests/CppInterOp/InterpreterTest.cpp index 8b527793ad1bc..e5edb32055a4c 100644 --- a/interpreter/CppInterOp/unittests/CppInterOp/InterpreterTest.cpp +++ b/interpreter/CppInterOp/unittests/CppInterOp/InterpreterTest.cpp @@ -84,27 +84,190 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, Interpreter_Evaluate) { // Due to a deficiency in the clang-repl implementation to get the value we // always must omit the ; TestFixture::CreateInterpreter(); - EXPECT_TRUE(Cpp::Evaluate("__cplusplus", nullptr) == 201402); + EXPECT_EQ(Cpp::Evaluate("__cplusplus").unbox(), 201402L); - bool HadError; - EXPECT_TRUE(Cpp::Evaluate("#error", &HadError) == (intptr_t)~0UL); - EXPECT_TRUE(HadError); + // K_Unspecified is the "no usable value" sentinel (parse error or + // no-Value-after-success). Replaces the old IsValueInvalid out-param. + EXPECT_EQ(Cpp::Evaluate("#error").getKind(), Cpp::Box::K_Unspecified); // for llvm < 19 this tests different overloads of // __clang_Interpreter_SetValueNoAlloc - EXPECT_EQ(Cpp::Evaluate("int i = 11; ++i", &HadError), 12); - EXPECT_FALSE(HadError); - EXPECT_EQ(Cpp::Evaluate("double a = 12.; a", &HadError), 12.); - EXPECT_FALSE(HadError); - EXPECT_EQ(Cpp::Evaluate("float b = 13.; b", &HadError), 13.); - EXPECT_FALSE(HadError); - EXPECT_EQ(Cpp::Evaluate("long double c = 14.; c", &HadError), 14.); - EXPECT_FALSE(HadError); - EXPECT_EQ(Cpp::Evaluate("long double d = 15.; d", &HadError), 15.); - EXPECT_FALSE(HadError); - EXPECT_EQ(Cpp::Evaluate("unsigned long long e = 16; e", &HadError), 16); - EXPECT_FALSE(HadError); - EXPECT_NE(Cpp::Evaluate("struct S{} s; s", &HadError), (intptr_t)~0UL); - EXPECT_FALSE(HadError); + EXPECT_EQ(Cpp::Evaluate("int i = 11; ++i").unbox(), 12); + EXPECT_EQ(Cpp::Evaluate("double a = 12.; a").unbox(), 12.); + EXPECT_EQ(Cpp::Evaluate("float b = 13.; b").unbox(), 13.f); + EXPECT_EQ(Cpp::Evaluate("long double c = 14.; c").unbox(), 14.L); + EXPECT_EQ(Cpp::Evaluate("long double d = 15.; d").unbox(), 15.L); + EXPECT_EQ( + Cpp::Evaluate("unsigned long long e = 16; e").unbox(), + 16ULL); + // Object payload: K_PtrOrObj carries an owned pointer to the JIT-managed + // instance; lifetime ends with the Value going out of scope. + Cpp::Box sV = Cpp::Evaluate("struct S{} s; s"); + EXPECT_EQ(sV.getKind(), Cpp::Box::K_PtrOrObj); + EXPECT_NE(sV.getObjectPtr(), nullptr); +} + +// Copy semantics mirror clang::Value: fundamentals POD-copy; K_PtrOrObj is +// refcounted-shallow (retain bumps a ref, dtor releases). The test exercises +// both: fundamentals copy independently, K_PtrOrObj copy shares the payload +// pointer and survives the original going out of scope. +TYPED_TEST(CPPINTEROP_TEST_MODE, Interpreter_Evaluate_Copy) { +#ifdef EMSCRIPTEN + GTEST_SKIP() << "Test fails for Emscipten builds"; +#endif +#ifdef _WIN32 + GTEST_SKIP() << "Disabled on Windows. Needs fixing."; +#endif + if (TypeParam::isOutOfProcess) + GTEST_SKIP() << "Test fails for OOP JIT builds"; + TestFixture::CreateInterpreter(); + + // Fundamentals: POD copy; original and copy hold equal data, independent. + Cpp::Box i = Cpp::Evaluate("int x = 42; x"); + Cpp::Box ic = i; + EXPECT_EQ(ic.getKind(), Cpp::Box::K_Int); + EXPECT_EQ(ic.unbox(), 42); + EXPECT_EQ(i.unbox(), 42); // original survives copy + + // K_PtrOrObj: shallow + refcounted. Copy shares the payload pointer; the + // payload survives the original going out of scope. Refcount reaches 0 + // only when both Values destruct (no double-free, no use-after-free under + // ASan). + void* sharedPtr = nullptr; + { + Cpp::Box sV = Cpp::Evaluate("struct CopyT { int v = 7; }; CopyT{}"); + ASSERT_EQ(sV.getKind(), Cpp::Box::K_PtrOrObj); + Cpp::Box sC = sV; + EXPECT_EQ(sC.getKind(), Cpp::Box::K_PtrOrObj); + EXPECT_EQ(sC.getObjectPtr(), sV.getObjectPtr()) + << "copy shares payload (refcount, not deep copy)"; + sharedPtr = sV.getObjectPtr(); + // sV goes out of inner scope here -- refcount drops to 1; sC still owns. + } + EXPECT_NE(sharedPtr, nullptr); + + // Non-trivial destructor: ValueStorage::Release runs the dtor via its + // function pointer when the last ref drops. ASan would flag a leak of + // the inner `int` if the refcounted copy fired the dtor twice (or not + // at all). Types are declared via Cpp::Declare (TU scope) so cling's + // destructor-thunk -- emitted in a later input_line -- can still + // resolve the name; declaring the struct inline inside Evaluate would + // make it a local struct on cling and break cross-TU lookup. + Cpp::Declare("struct DtorT { int* p; DtorT() { p = new int(13); } " + "~DtorT() { delete p; } };"); + { + Cpp::Box dV = Cpp::Evaluate("DtorT{}"); + ASSERT_EQ(dV.getKind(), Cpp::Box::K_PtrOrObj); + { Cpp::Box dC = dV; } // copy then drop -- refcount 2 -> 1; no dtor + // dV still owns; dtor fires when this scope ends. + } + + // Class with a static member: the payload's per-instance heap path + // must not double-count the static, and the dtor pointer fires only + // for the instance. (Statics live in module data, not the payload.) + // TU-scope declaration is required: cling wraps Evaluate snippets in + // a function, and C++ forbids static data members in local types. + Cpp::Declare("int StatT_freed = 0; " + "struct StatT { static int counter; int* p; " + "StatT() { p = new int(++counter); } " + "~StatT() { delete p; ++StatT_freed; } }; " + "int StatT::counter = 0;"); + { + Cpp::Box sV = Cpp::Evaluate("StatT{}"); + ASSERT_EQ(sV.getKind(), Cpp::Box::K_PtrOrObj); + { Cpp::Box sC = sV; } // refcounted copy; no dtor on drop + } + // After the box drops, exactly one dtor fired (no spurious second + // delete from the now-released copy). + EXPECT_EQ(Cpp::Evaluate("StatT_freed").unbox(), 1); + + // Move semantics: src is reset to K_Unspecified so its dtor does not + // release the storage the destination now owns. A regression here + // would double-release the K_PtrOrObj payload (ASan-detectable). + Cpp::Declare("struct MoveT { int v = 1; };"); + Cpp::Box src = Cpp::Evaluate("MoveT{}"); + ASSERT_EQ(src.getKind(), Cpp::Box::K_PtrOrObj); + void* origPtr = src.getObjectPtr(); + Cpp::Box dst = std::move(src); + EXPECT_EQ(src.getKind(), Cpp::Box::K_Unspecified); + EXPECT_EQ(dst.getObjectPtr(), origPtr); +} + +// visit() dispatches over Kind to the matching typed slot. Test in two +// shapes: (a) Kind known statically -- the AOT-fold path that catalog +// thunks ride, where Create().visit(v) constant-folds to v(x); +// (b) Kind known only at runtime -- the GenericExecutor pattern, where +// the visitor must accept every fundamental T. +TYPED_TEST(CPPINTEROP_TEST_MODE, Interpreter_Evaluate_VisitDispatch) { +#ifdef EMSCRIPTEN + GTEST_SKIP() << "Test fails for Emscipten builds"; +#endif +#ifdef _WIN32 + GTEST_SKIP() << "Disabled on Windows. Needs fixing."; +#endif + if (TypeParam::isOutOfProcess) + GTEST_SKIP() << "Test fails for OOP JIT builds"; + TestFixture::CreateInterpreter(); + + // (a) AOT fold: Kind is K_Int by construction. The visitor receives an + // int and routes through the K_Int case only. + auto toll = [](auto x) -> long long { return static_cast(x); }; + EXPECT_EQ(Cpp::Box::Create(42).visit(toll), 42LL); + EXPECT_EQ(Cpp::Box::Create(3.5).visit(toll), 3LL); // trunc to ll + EXPECT_EQ(Cpp::Box::Create(true).visit(toll), 1LL); + + // (b) Runtime: Kind is data, set by Evaluate. visit dispatches on the + // runtime tag; the same visitor handles every fundamental. + EXPECT_EQ(Cpp::Evaluate("int i = 11; ++i").visit(toll), 12LL); + EXPECT_EQ(Cpp::Evaluate("double a = 12.5; a").visit(toll), 12LL); + EXPECT_EQ(Cpp::Evaluate("unsigned long long u = 16; u").visit(toll), 16LL); + + // convertTo is the public "I don't know the Kind, give me T" path. + // Internally it visit()s and static_casts -- so the same dispatch + // covers fundamentals safely without the Kind-mismatch UB of as. + // Distinct decl names keep each Evaluate from being a redeclaration + // of the previous (which would parse-error to K_Unspecified and trip + // visit's __builtin_unreachable). + EXPECT_EQ(Cpp::Evaluate("int cv_a = 7; cv_a").convertTo(), 7L); + EXPECT_EQ(Cpp::Evaluate("int cv_b = 7; cv_b").convertTo(), 7.0); + EXPECT_EQ(Cpp::Evaluate("double cv_c = 4.5; cv_c").convertTo(), 4); +} + +// Covers every BuiltinType::Kind arm of the QualType classifier in +// Evaluate. Each line exercises a distinct branch of the switch in +// CppInterOp.cpp's classifyByQualType. +TYPED_TEST(CPPINTEROP_TEST_MODE, Interpreter_Evaluate_AllFundamentals) { +#ifdef EMSCRIPTEN + GTEST_SKIP() << "Test fails for Emscipten builds"; +#endif +#ifdef _WIN32 + GTEST_SKIP() << "Disabled on Windows. Needs fixing."; +#endif + if (TypeParam::isOutOfProcess) + GTEST_SKIP() << "Test fails for OOP JIT builds"; + TestFixture::CreateInterpreter(); + + EXPECT_EQ(Cpp::Evaluate("bool b = true; b").unbox(), true); + EXPECT_EQ(Cpp::Evaluate("signed char sc = -3; sc").unbox(), -3); + EXPECT_EQ(Cpp::Evaluate("unsigned char uc = 7; uc").unbox(), + 7); + EXPECT_EQ(Cpp::Evaluate("short sh = -9; sh").unbox(), -9); + EXPECT_EQ(Cpp::Evaluate("unsigned short us = 11; us").unbox(), + 11); + EXPECT_EQ(Cpp::Evaluate("unsigned int ui = 13; ui").unbox(), + 13U); + EXPECT_EQ(Cpp::Evaluate("unsigned long ul = 17; ul").unbox(), + 17UL); + EXPECT_EQ(Cpp::Evaluate("long long ll = -21; ll").unbox(), -21LL); + + // Plain `char` resolves to Char_S on platforms where char is signed + // by default (x86_64 Linux/macOS, MSVC) and Char_U on platforms where + // it is unsigned (Linux/aarch64, PowerPC); the classifier folds the + // latter into K_UChar so the Box::Create X-macro stays single- + // valued per C++ type. + Cpp::Box c = Cpp::Evaluate("char ch = 'a'; ch"); + EXPECT_TRUE(c.getKind() == Cpp::Box::K_Char_S || + c.getKind() == Cpp::Box::K_UChar); + EXPECT_EQ(c.convertTo(), 'a'); } // Regression (cppyy test12): no-Value-after-success used to abort in @@ -121,18 +284,16 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, Interpreter_Evaluate_NonValueStatement) { GTEST_SKIP() << "Test fails for OOP JIT builds"; TestFixture::CreateInterpreter(); - bool HadError = false; - EXPECT_EQ(Cpp::Evaluate("class EvalRegression_NoValue {};", &HadError), - (intptr_t)~0UL); - EXPECT_TRUE(HadError); + EXPECT_EQ(Cpp::Evaluate("class EvalRegression_NoValue {};").getKind(), + Cpp::Box::K_Unspecified); } TYPED_TEST(CPPINTEROP_TEST_MODE, Interpreter_DeleteInterpreter) { if (TypeParam::isOutOfProcess) GTEST_SKIP() << "Test fails for OOP JIT builds"; - auto* I1 = TestFixture::CreateInterpreter(); - auto* I2 = TestFixture::CreateInterpreter(); - auto* I3 = TestFixture::CreateInterpreter(); + auto I1 = TestFixture::CreateInterpreter(); + auto I2 = TestFixture::CreateInterpreter(); + auto I3 = TestFixture::CreateInterpreter(); EXPECT_TRUE(I1 && I2 && I3) << "Failed to create interpreters"; EXPECT_EQ(I3, Cpp::GetInterpreter()) << "I3 is not active"; @@ -140,7 +301,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, Interpreter_DeleteInterpreter) { EXPECT_TRUE(Cpp::DeleteInterpreter(/*I=*/nullptr)); EXPECT_EQ(I2, Cpp::GetInterpreter()); - auto* I4 = reinterpret_cast(static_cast(~0U)); + Cpp::InterpRef I4{reinterpret_cast(static_cast(~0U))}; EXPECT_FALSE(Cpp::DeleteInterpreter(I4)); EXPECT_TRUE(Cpp::DeleteInterpreter(I1)); @@ -154,8 +315,8 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, Interpreter_StaticDtorsRunOnDelete) { if (TypeParam::isOutOfProcess) GTEST_SKIP() << "Test fails for OOP JIT builds"; - auto* I = TestFixture::CreateInterpreter(); - ASSERT_NE(I, nullptr); + auto I = TestFixture::CreateInterpreter(); + ASSERT_TRUE(I); // Host-side flag the JIT writes to; survives the interpreter deletion // that frees JIT memory. Reset explicitly for --gtest_repeat. @@ -164,11 +325,10 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, Interpreter_StaticDtorsRunOnDelete) { std::string Inject = "extern \"C\" void* dtor_sink = (void*)" + std::to_string(reinterpret_cast(&HostFlag)) + ";"; - Cpp::Declare(Inject.c_str(), I); + Cpp::Declare(Inject.c_str()); Cpp::Declare(R"( struct DtorNotify { ~DtorNotify() { *static_cast(dtor_sink) = 1; } }; - )", - I); + )"); Cpp::Process("static DtorNotify notify;"); EXPECT_EQ(HostFlag, 0); @@ -203,8 +363,8 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, #else EXPECT_EXIT( { - auto* I1 = TestFixture::CreateInterpreter(); - auto* I2 = TestFixture::CreateInterpreter(); + auto I1 = TestFixture::CreateInterpreter(); + auto I2 = TestFixture::CreateInterpreter(); Cpp::ActivateInterpreter(I1); Cpp::Process("struct S1 { S1() {} ~S1() {} }; static S1 s1;"); Cpp::ActivateInterpreter(I2); @@ -222,25 +382,26 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, Interpreter_ActivateInterpreter) { if (TypeParam::isOutOfProcess) GTEST_SKIP() << "Test fails for OOP JIT builds"; EXPECT_FALSE(Cpp::ActivateInterpreter(nullptr)); - auto* Cpp14 = TestFixture::CreateInterpreter({"-std=c++14"}); - auto* Cpp17 = TestFixture::CreateInterpreter({"-std=c++17"}); - auto* Cpp20 = TestFixture::CreateInterpreter({"-std=c++20"}); + auto Cpp14 = TestFixture::CreateInterpreter({"-std=c++14"}); + auto Cpp17 = TestFixture::CreateInterpreter({"-std=c++17"}); + auto Cpp20 = TestFixture::CreateInterpreter({"-std=c++20"}); EXPECT_TRUE(Cpp14 && Cpp17 && Cpp20); - EXPECT_TRUE(Cpp::Evaluate("__cplusplus") == 202002L) + EXPECT_EQ(Cpp::Evaluate("__cplusplus").unbox(), 202002L) << "Failed to activate C++20"; - auto* UntrackedI = reinterpret_cast(static_cast(~0U)); + Cpp::InterpRef UntrackedI{ + reinterpret_cast(static_cast(~0U))}; EXPECT_FALSE(Cpp::ActivateInterpreter(UntrackedI)); EXPECT_TRUE(Cpp::ActivateInterpreter(Cpp14)); - EXPECT_TRUE(Cpp::Evaluate("__cplusplus") == 201402L); + EXPECT_EQ(Cpp::Evaluate("__cplusplus").unbox(), 201402L); Cpp::DeleteInterpreter(Cpp14); EXPECT_EQ(Cpp::GetInterpreter(), Cpp20); EXPECT_TRUE(Cpp::ActivateInterpreter(Cpp17)); - EXPECT_TRUE(Cpp::Evaluate("__cplusplus") == 201703L); + EXPECT_EQ(Cpp::Evaluate("__cplusplus").unbox(), 201703L); } TYPED_TEST(CPPINTEROP_TEST_MODE, Interpreter_Process) { @@ -327,7 +488,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, Interpreter_EmscriptenExceptionHandling) { } TYPED_TEST(CPPINTEROP_TEST_MODE, Interpreter_CreateInterpreter) { - auto* I = TestFixture::CreateInterpreter(); + auto I = TestFixture::CreateInterpreter(); EXPECT_TRUE(I); // Check if the default standard is c++14 @@ -560,7 +721,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, SignalHandler_BasicBanner) { // EXPECT_FALSE(Cpp::GetInterpreter()) << "Failed to delete all interpreters"; // // Create an interpreter (this calls RegisterInterpreter internally) - // TInterp_t I = TestFixture::CreateInterpreter(); + // InterpRef I = TestFixture::CreateInterpreter(); // ASSERT_NE(I, nullptr); // We expect the banner to appear in stderr when the process dies @@ -582,8 +743,8 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, SignalHandler_BasicBanner) { // Verify that the handler correctly lists multiple interpreters #ifdef GTEST_HAS_DEATH_TEST TYPED_TEST(CPPINTEROP_TEST_MODE, SignalHandler_MultipleInterpreters) { - ASSERT_NE(TestFixture::CreateInterpreter(), nullptr); - ASSERT_NE(TestFixture::CreateInterpreter(), nullptr); + ASSERT_TRUE(TestFixture::CreateInterpreter()); + ASSERT_TRUE(TestFixture::CreateInterpreter()); // The handler iterates through the deque and prints the pointers @@ -604,15 +765,15 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, Interpreter_WrapperCacheIsPerInterpreter) { GTEST_SKIP() << "Test fails for OOP JIT builds"; // Create first interpreter: add(a,b) returns a + b. - auto* I1 = TestFixture::CreateInterpreter(); - ASSERT_NE(I1, nullptr); + auto I1 = TestFixture::CreateInterpreter(); + ASSERT_TRUE(I1); Cpp::ActivateInterpreter(I1); Cpp::Declare("int add(int a, int b) { return a + b; }"); - auto* AddDecl1 = Cpp::GetNamed("add"); - ASSERT_NE(AddDecl1, nullptr); + auto AddDecl1 = Cpp::GetNamed("add"); + ASSERT_TRUE(AddDecl1); Cpp::Declare("#include "); // Needed by JitCall - auto JC1 = Cpp::MakeFunctionCallable(AddDecl1); + auto JC1 = Cpp::MakeFunctionCallable(Cpp::FuncRef{AddDecl1.data}); ASSERT_TRUE(JC1.isValid()); int a1 = 3, b1 = 4, r1 = 0; @@ -621,15 +782,15 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, Interpreter_WrapperCacheIsPerInterpreter) { EXPECT_EQ(r1, 7); // Create second interpreter: add(a,b) returns a * b. - auto* I2 = TestFixture::CreateInterpreter(); - ASSERT_NE(I2, nullptr); + auto I2 = TestFixture::CreateInterpreter(); + ASSERT_TRUE(I2); Cpp::ActivateInterpreter(I2); Cpp::Declare("int add(int a, int b) { return a * b; }"); - auto* AddDecl2 = Cpp::GetNamed("add"); - ASSERT_NE(AddDecl2, nullptr); + auto AddDecl2 = Cpp::GetNamed("add"); + ASSERT_TRUE(AddDecl2); Cpp::Declare("#include "); // Needed by JitCall - auto JC2 = Cpp::MakeFunctionCallable(AddDecl2); + auto JC2 = Cpp::MakeFunctionCallable(Cpp::FuncRef{AddDecl2.data}); ASSERT_TRUE(JC2.isValid()); // Delete the first interpreter. @@ -663,8 +824,20 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, Interpreter_DLMIsPerInterpreter) { ASSERT_FALSE(BinaryPath.empty()); llvm::StringRef BinDir = llvm::sys::path::parent_path(BinaryPath); - auto* I1 = TestFixture::CreateInterpreter(); - ASSERT_NE(I1, nullptr); + // Baseline: what a pristine interpreter (no user-added search path) sees. + // Every DLM defaults to searching "." (the cwd); when the binary's cwd + // contains TestSharedLib.so (e.g. under a runfiles-style layout) a fresh + // interpreter already resolves kSym, and when it doesn't (e.g. cwd is a + // separate build dir) it resolves nothing. Capturing this makes the test + // assert about path *leakage*, not about the cwd-dependent default scan. + auto I0 = TestFixture::CreateInterpreter(); + ASSERT_TRUE(I0); + Cpp::ActivateInterpreter(I0); + std::string R0 = Cpp::SearchLibrariesForSymbol(kSym, /*system_search=*/false); + EXPECT_TRUE(Cpp::DeleteInterpreter(I0)); + + auto I1 = TestFixture::CreateInterpreter(); + ASSERT_TRUE(I1); Cpp::ActivateInterpreter(I1); Cpp::AddSearchPath(BinDir.str().c_str()); @@ -672,15 +845,16 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, Interpreter_DLMIsPerInterpreter) { if (R1.empty()) GTEST_SKIP() << "TestSharedLib not found — cannot test DLM isolation"; - // With a single function-local-static DLM, the path added on I1 - // would reach I2 and the lookup below would succeed; the per- - // interpreter DLM means I2 starts with a fresh path set. - auto* I2 = TestFixture::CreateInterpreter(); - ASSERT_NE(I2, nullptr); + // A fresh I2 has its own DLM and must NOT inherit the path added on I1. + // With a single shared DLM, I2 would also see BinDir and so differ from the + // pristine baseline I0; per-interpreter DLM means I2 behaves like I0. + auto I2 = TestFixture::CreateInterpreter(); + ASSERT_TRUE(I2); Cpp::ActivateInterpreter(I2); std::string R2 = Cpp::SearchLibrariesForSymbol(kSym, /*system_search=*/false); - EXPECT_TRUE(R2.empty()) << "I2 must not see search paths added on I1; found: " - << R2; + EXPECT_EQ(R2, R0) << "I2 must behave like a pristine interpreter; the search " + "path added on I1 must not leak. I2 found: '" + << R2 << "', pristine baseline: '" << R0 << "'"; } #endif // !EMSCRIPTEN diff --git a/interpreter/CppInterOp/unittests/CppInterOp/PerfCompare.h b/interpreter/CppInterOp/unittests/CppInterOp/PerfCompare.h new file mode 100644 index 0000000000000..79fdb088c1ea9 --- /dev/null +++ b/interpreter/CppInterOp/unittests/CppInterOp/PerfCompare.h @@ -0,0 +1,67 @@ +// gtest-facing relative-performance assertions over Google Benchmark. +// +// One BENCHMARK per dispatch tier; one TEST per ordering claim. The +// first macro use runs all registered benchmarks once (auto-iterated, +// median-quantile), caches the results by name through a custom +// reporter, and later uses hit the cache. Assertions are relative +// (X-not-slower-than-Y), which is stable across hardware where +// absolute ns are not. + +#ifndef CPPINTEROP_TEST_PERFCOMPARE_H +#define CPPINTEROP_TEST_PERFCOMPARE_H + +#include +#include "gtest/gtest.h" + +#include +#include +#include + +namespace PerfCompare { + +inline const std::map& BenchmarkResults() { + static std::map cache; + static bool ran = false; + if (!ran) { + struct Collect : benchmark::BenchmarkReporter { + std::map& out; + explicit Collect(std::map& m) : out(m) {} + bool ReportContext(const Context&) override { return true; } + void ReportRuns(const std::vector& runs) override { + for (const auto& r : runs) + out[r.benchmark_name()] = r.GetAdjustedCPUTime() / r.iterations * 1e9; + } + }; + Collect c(cache); + benchmark::RunSpecifiedBenchmarks(&c); + ran = true; + } + return cache; +} + +inline double NsPerOp(const std::string& name) { + const auto& r = BenchmarkResults(); + auto it = r.find(name); + if (it == r.end()) { + ADD_FAILURE() << "Benchmark not registered: " << name; + return 0.0; + } + return it->second; +} + +} // namespace PerfCompare + +// A should be at least `factor` times faster than B. +#define EXPECT_AT_LEAST_N_TIMES_FASTER(A, B, factor) \ + do { \ + double ta_ = ::PerfCompare::NsPerOp(#A); \ + double tb_ = ::PerfCompare::NsPerOp(#B); \ + EXPECT_GT(tb_ / ta_, (factor)) \ + << #A << " (" << ta_ << " ns) not " << (factor) << "x faster than " \ + << #B << " (" << tb_ << " ns)"; \ + } while (0) + +// A should not be slower than B (10% tolerance for measurement noise). +#define EXPECT_NOT_SLOWER_THAN(A, B) EXPECT_AT_LEAST_N_TIMES_FASTER(A, B, 0.9) + +#endif // CPPINTEROP_TEST_PERFCOMPARE_H diff --git a/interpreter/CppInterOp/unittests/CppInterOp/ResultTest.cpp b/interpreter/CppInterOp/unittests/CppInterOp/ResultTest.cpp new file mode 100644 index 0000000000000..3af1607b450a9 --- /dev/null +++ b/interpreter/CppInterOp/unittests/CppInterOp/ResultTest.cpp @@ -0,0 +1,323 @@ +//===- ResultTest.cpp - Tests for Cpp::Result ---------------*- C++ -*-===// +// +// Part of the compiler-research project, under the Apache License v2.0 with +// LLVM Exceptions. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +// +// Value-semantic tests for Cpp::Result: construction, accessors, +// copy/move (including the Unchecked-flag transfer that catches the +// moved-from-aborts-in-dtor trap), .ignore() opt-out, and death paths +// for dropped errors and value()-on-error. +// +//===----------------------------------------------------------------------===// + +#include "CppInterOp/CppInterOp.h" +#include "CppInterOp/CppInterOpTypes.h" +#include "CppInterOp/Error.h" + +#include "../../lib/CppInterOp/ErrorInternal.h" +#include "../../lib/CppInterOp/InterpreterInfo.h" +#include "../../lib/CppInterOp/Tracing.h" // INTEROP_FUNC_SIG + +#include "Utils.h" + +#include "llvm/Support/Error.h" + +#include "gtest/gtest.h" + +#include +#include + +// Size invariants. Each row caps Cpp::Result at llvm::Expected's +// size; a refactor that inflates the layout fails the build. + +static_assert(sizeof(Cpp::ErrorRef) == sizeof(void*), + "ErrorRef must be a single pointer"); + +// Result in release is just the ErrorRef pointer. Debug adds the +// Unchecked bit; allow up to llvm::Error's debug-fudge ceiling. +static_assert(sizeof(Cpp::Result) <= sizeof(llvm::Error) * 2, + "Result should not exceed llvm::Error layout"); + +static_assert(sizeof(Cpp::Result) <= + sizeof(llvm::Expected), + "Result must not exceed llvm::Expected"); + +static_assert(sizeof(Cpp::Result) <= sizeof(llvm::Expected), + "Result must not exceed llvm::Expected"); + +namespace { +// Inline-encoded Status::NotFound: round-trips through the tag bit, +// allocates nothing, costs no refcount work. +Cpp::ErrorRef MakeErr() { + return Cpp::ErrorRef::makeInline(Cpp::Status::NotFound); +} +} // namespace + +TEST(ResultTest, Result_OkFromValue) { + Cpp::Result R{42}; + EXPECT_TRUE(R.ok()); + EXPECT_TRUE(static_cast(R)); + EXPECT_EQ(R.status(), Cpp::Status::Ok); + EXPECT_TRUE(R.error().isOk()); + EXPECT_EQ(R.value(), 42); + EXPECT_EQ(R.value_or(-1), 42); +} + +TEST(ResultTest, Result_DefaultIsOk) { + // Default-constructed Result holds a default-constructed T; ok(). + Cpp::Result R; + EXPECT_TRUE(R.ok()); + EXPECT_EQ(R.value(), 0); +} + +TEST(ResultTest, Result_ErrorFromErrorRef) { + Cpp::Result R{MakeErr()}; + EXPECT_FALSE(R.ok()); + EXPECT_FALSE(static_cast(R)); + EXPECT_EQ(R.status(), Cpp::Status::NotFound); + EXPECT_FALSE(R.error().isOk()); + EXPECT_TRUE(R.error().isInline()); + EXPECT_FALSE(R.error().isSlice()); + EXPECT_EQ(R.value_or(-1), -1); +} + +TEST(ResultTest, ResultVoid_DefaultIsOk) { + Cpp::Result R; + EXPECT_TRUE(R.ok()); + EXPECT_EQ(R.status(), Cpp::Status::Ok); + EXPECT_TRUE(R.error().isOk()); +} + +TEST(ResultTest, ResultVoid_FromErrorRef) { + Cpp::Result R{MakeErr()}; + EXPECT_FALSE(R.ok()); + EXPECT_EQ(R.status(), Cpp::Status::NotFound); + EXPECT_TRUE(R.error().isInline()); +} + +TEST(ResultTest, Copy_Ok) { + Cpp::Result A{7}; + // NOLINTNEXTLINE(performance-unnecessary-copy-initialization) + Cpp::Result B{A}; // exercises the copy ctor; do not collapse to a ref + EXPECT_TRUE(B.ok()); + EXPECT_EQ(B.value(), 7); + // The source remains usable after copy; we explicitly check it here + // so its dtor doesn't trip the must-handle guard. + EXPECT_EQ(A.value(), 7); +} + +TEST(ResultTest, Copy_Error) { + Cpp::Result A{MakeErr()}; + // NOLINTNEXTLINE(performance-unnecessary-copy-initialization) + Cpp::Result B{A}; // exercises the copy ctor; do not collapse to a ref + EXPECT_FALSE(B.ok()); + EXPECT_FALSE(A.ok()); // mark A checked so its dtor doesn't abort +} + +TEST(ResultTest, Move_Ok) { + Cpp::Result A{11}; + Cpp::Result B{std::move(A)}; + EXPECT_TRUE(B.ok()); + EXPECT_EQ(B.value(), 11); + // A was Ok-valued; the move-from state is undefined for the value + // but still ok-tagged. No abort on A's dtor. +} + +// Pins the bug where Result's default move ctor failed to +// transfer the Unchecked obligation, causing the source's dtor to +// abort even though the value semantically went to the destination +// (most visible when threaded through a tracing record() wrapper). +TEST(ResultTest, MoveCtor_Void_TransfersUncheckedToSource) { + Cpp::Result A{MakeErr()}; + Cpp::Result B{std::move(A)}; + // Check only B; A must not abort at scope exit despite never being + // inspected directly. + EXPECT_FALSE(B.ok()); +} + +TEST(ResultTest, MoveCtor_NonVoid_TransfersUncheckedToSource) { + Cpp::Result A{MakeErr()}; + Cpp::Result B{std::move(A)}; + EXPECT_FALSE(B.ok()); +} + +// Non-trivial T exercises the placement-new + explicit destructor +// paths in ctor/dtor. Result alone wouldn't catch a missing +// T::~T() call -- int has no destructor to miss. ASan / leak checker +// flags any miss here. +TEST(ResultTest, Result_NonTrivialT_PlacementNewAndDtor) { + { + Cpp::Result R{std::string("hello")}; + EXPECT_TRUE(R.ok()); + EXPECT_EQ(R.value(), "hello"); + } + + Cpp::Result A{std::string("world")}; + Cpp::Result B{std::move(A)}; + EXPECT_TRUE(B.ok()); + EXPECT_EQ(B.value(), "world"); +} + +// .ignore() silences the dropped-error abort. Without it, the inner +// blocks would die at scope exit. +TEST(ResultTest, Ignore_SuppressesDroppedErrorAbort) { + { + Cpp::Result R{MakeErr()}; + R.ignore(); + } + { + Cpp::Result R{MakeErr()}; + R.ignore(); + } + SUCCEED(); +} + +#ifndef EMSCRIPTEN + +#ifndef NDEBUG +TEST(ResultDeathTest, DroppedError_Aborts) { + ::testing::FLAGS_gtest_death_test_style = "threadsafe"; + EXPECT_DEATH({ (void)Cpp::Result(MakeErr()); }, + ".*destroyed without check.*"); +} +#endif // !NDEBUG + +// value() on an error always aborts, debug or release. +TEST(ResultDeathTest, ValueOnError_Aborts) { + ::testing::FLAGS_gtest_death_test_style = "threadsafe"; + EXPECT_DEATH({ (void)Cpp::Result(MakeErr()).value(); }, + ".*value\\(\\) called on an error-bearing Result.*"); +} + +#endif // !EMSCRIPTEN + +// +// Every Status value round-trips through makeInline -> ErrorRef bits -> +// status(). Status::Ok canonicalises to the null encoding; the rest +// pack into a single uintptr_t with no allocation. + +TEST(ResultTest, InlineEncoding_StatusRoundTrip) { + const Cpp::Status All[] = { + Cpp::Status::NotFound, Cpp::Status::InvalidArgument, + Cpp::Status::Ambiguous, Cpp::Status::ParseError, + Cpp::Status::CompileError, + }; + for (Cpp::Status S : All) { + Cpp::ErrorRef E = Cpp::ErrorRef::makeInline(S); + EXPECT_FALSE(E.isOk()); + EXPECT_TRUE(E.isInline()); + EXPECT_FALSE(E.isSlice()); + EXPECT_EQ(E.status(), S); + // Inline errors carry no body. + EXPECT_EQ(E.diagnostics().size(), 0U); + EXPECT_EQ(E.producer(), nullptr); + } +} + +TEST(ResultTest, InlineEncoding_OkCanonicalisesToNull) { + Cpp::ErrorRef E = Cpp::ErrorRef::makeInline(Cpp::Status::Ok); + EXPECT_TRUE(E.isOk()); + EXPECT_FALSE(E.isInline()); + EXPECT_FALSE(E.isSlice()); + EXPECT_EQ(E.bits, 0U); +} + +namespace { +// makeError / drainError need a live interpreter to allocate slices +// against. +struct ResultBoundaryTest : public ::testing::Test { + void SetUp() override { + if (!Cpp::GetInterpreter()) + Cpp::CreateInterpreter(); + } +}; +} // namespace + +TEST_F(ResultBoundaryTest, MakeError_SliceCarriesMessageAndProducer) { + Cpp::InterpreterInfo* II = Cpp::GetInterpInfo(); + Cpp::ClearPending(II); + Cpp::Result R{Cpp::makeError( + II, Cpp::Status::ParseError, "syntax error", __func__, INTEROP_FUNC_SIG)}; + ASSERT_FALSE(R.ok()); + EXPECT_EQ(R.status(), Cpp::Status::ParseError); + EXPECT_TRUE(R.error().isSlice()); + EXPECT_FALSE(R.error().isInline()); + ASSERT_GE(R.error().diagnostics().size(), 1U); + EXPECT_STREQ(R.error().diagnostics()[0].message(), "syntax error"); + EXPECT_EQ(R.error().diagnostics()[0].severity(), + Cpp::DiagnosticSeverity::Error); + EXPECT_STREQ(R.error().producer(), __func__); +} + +// drainError lifts an llvm::Error chain into a slice, preserving Status +// and any structured payload the chain carries. +TEST_F(ResultBoundaryTest, DrainError_StatusErrorFixesCode) { + Cpp::InterpreterInfo* II = Cpp::GetInterpInfo(); + Cpp::ClearPending(II); + llvm::Error E = llvm::make_error(Cpp::Status::NotFound, + "no such symbol"); + Cpp::Result R{Cpp::drainError(II, std::move(E), __func__, nullptr)}; + ASSERT_FALSE(R.ok()); + EXPECT_EQ(R.status(), Cpp::Status::NotFound); +} + +// share() bumps the slice refcount so the captured error survives the +// originating Result's destruction. +TEST_F(ResultBoundaryTest, CapturedError_OutlivesResult) { + Cpp::InterpreterInfo* II = Cpp::GetInterpInfo(); + Cpp::ClearPending(II); + Cpp::CapturedError C; + size_t DiagCount = 0; + { + Cpp::Result R{Cpp::makeError( + II, Cpp::Status::ParseError, "captured-error test", __func__, nullptr)}; + ASSERT_FALSE(R.ok()); + DiagCount = R.error().diagnostics().size(); + C = R.share(); + } // R out of scope; slice survives because C holds a ref. + EXPECT_FALSE(C.ok()); + EXPECT_EQ(C.status(), Cpp::Status::ParseError); + EXPECT_EQ(C.diagnostics().size(), DiagCount); +} + +// Inline errors have no slice; capture is a pure value copy with no +// refcount bookkeeping. +TEST(ResultTest, CapturedError_InlineHasNoSlice) { + Cpp::Result R{Cpp::ErrorRef::makeInline(Cpp::Status::InvalidArgument)}; + Cpp::CapturedError C = R.share(); + EXPECT_FALSE(C.ok()); + EXPECT_EQ(C.status(), Cpp::Status::InvalidArgument); + EXPECT_TRUE(C.ref().isInline()); + EXPECT_FALSE(C.ref().isSlice()); +} + +// record() copies into an owning value-type so the caller can drop the +// Result and the slice can be freed. +TEST_F(ResultBoundaryTest, ErrorRecord_OwnsDiagnostics) { + Cpp::ErrorRecord Rec; + { + Cpp::InterpreterInfo* II = Cpp::GetInterpInfo(); + Cpp::ClearPending(II); + Cpp::Result R{Cpp::makeError( + II, Cpp::Status::ParseError, "recordable message", __func__, nullptr)}; + ASSERT_FALSE(R.ok()); + Rec = R.error().record(); + } // R dies; slice freed. Rec stays usable. + EXPECT_EQ(Rec.Code, Cpp::Status::ParseError); + ASSERT_GE(Rec.Diagnostics.size(), 1U); + EXPECT_EQ(Rec.Diagnostics[0].Message, "recordable message"); +} + +TEST(ResultTest, ErrorRecord_InlineProducesEmpty) { + Cpp::ErrorRef E = Cpp::ErrorRef::makeInline(Cpp::Status::InvalidArgument); + Cpp::ErrorRecord Rec = E.record(); + EXPECT_EQ(Rec.Code, Cpp::Status::InvalidArgument); + EXPECT_TRUE(Rec.Diagnostics.empty()); +} + +static_assert(sizeof(Cpp::ErrorRef) == sizeof(void*), + "ErrorRef stays one pointer-word"); +static_assert(sizeof(Cpp::Status) == 1, "Status enum stays one byte"); diff --git a/interpreter/CppInterOp/unittests/CppInterOp/ScopeReflectionTest.cpp b/interpreter/CppInterOp/unittests/CppInterOp/ScopeReflectionTest.cpp index 36e4ea43240be..9ad8c1d20f6ac 100644 --- a/interpreter/CppInterOp/unittests/CppInterOp/ScopeReflectionTest.cpp +++ b/interpreter/CppInterOp/unittests/CppInterOp/ScopeReflectionTest.cpp @@ -33,10 +33,10 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, ScopeReflection_GetTypeOfBuiltins) { if (!QT.isNull()) { \ std::string name = QT.getAsString(C.getPrintingPolicy()); \ if (name[0] != '<' && !QT->isPlaceholderType()) { \ - const auto* FoundTy = Cpp::GetType(name); \ + auto FoundTy = Cpp::GetType(name); \ EXPECT_TRUE(FoundTy) << "Failed to find builtin: '" << name << "'"; \ if (FoundTy) { \ - EXPECT_EQ(FoundTy, QT.getAsOpaquePtr()) \ + EXPECT_EQ(FoundTy.data, QT.getAsOpaquePtr()) \ << "Type mismatch for '" << name << "'"; \ EXPECT_TRUE(Cpp::IsBuiltin(FoundTy)); \ } \ @@ -47,8 +47,8 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, ScopeReflection_GetTypeOfBuiltins) { #undef BUILTIN_TYPE auto Verify = [&](const char* Name, QualType Expected) { - void* Found = Cpp::GetType(Name); - EXPECT_EQ(Found, Expected.getAsOpaquePtr()) << "Failed for: " << Name; + auto Found = Cpp::GetType(Name); + EXPECT_EQ(Found.data, Expected.getAsOpaquePtr()) << "Failed for: " << Name; }; // --- Integer Variations --- @@ -284,7 +284,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, ScopeReflection_IsComplete) { EXPECT_FALSE(Cpp::IsComplete(Decls[3])); EXPECT_FALSE(Cpp::IsComplete(Decls[4])); EXPECT_TRUE(Cpp::IsComplete(Decls[5])); - Cpp::TCppType_t retTy = Cpp::GetFunctionReturnType(Decls[7]); + Cpp::TypeRef retTy = Cpp::GetFunctionReturnType(Decls[7]); EXPECT_TRUE(Cpp::IsComplete(Cpp::GetScopeFromType(retTy))); EXPECT_FALSE(Cpp::IsComplete(nullptr)); } @@ -511,7 +511,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, ScopeReflection_GetCompleteName) { ASTContext& C = Interp->getCI()->getASTContext(); std::vector template_args = { C.IntTy.getAsOpaquePtr(), C.DoubleTy.getAsOpaquePtr()}; - Cpp::TCppScope_t fn = Cpp::InstantiateTemplate(Decls[11], template_args); + Cpp::DeclRef fn = Cpp::InstantiateTemplate(Decls[11], template_args); EXPECT_TRUE(fn); EXPECT_EQ(Cpp::GetCompleteName(fn), "fn"); } @@ -529,7 +529,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, ScopeReflection_GetQualifiedName) { GetAllSubDecls(Decls[0], Decls); GetAllSubDecls(Decls[1], Decls); - EXPECT_EQ(Cpp::GetQualifiedName(0), ""); + EXPECT_EQ(Cpp::GetQualifiedName(nullptr), ""); EXPECT_EQ(Cpp::GetQualifiedName(Decls[0]), "N"); EXPECT_EQ(Cpp::GetQualifiedName(Decls[1]), "N::C"); EXPECT_EQ(Cpp::GetQualifiedName(Decls[3]), "N::C::i"); @@ -552,7 +552,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, ScopeReflection_GetQualifiedCompleteName) { GetAllSubDecls(Decls[0], Decls); GetAllSubDecls(Decls[1], Decls); - EXPECT_EQ(Cpp::GetQualifiedCompleteName(0), ""); + EXPECT_EQ(Cpp::GetQualifiedCompleteName(nullptr), ""); EXPECT_EQ(Cpp::GetQualifiedCompleteName(Decls[0]), "N"); EXPECT_EQ(Cpp::GetQualifiedCompleteName(Decls[1]), "N::C"); EXPECT_EQ(Cpp::GetQualifiedCompleteName(Decls[2]), "N::A"); @@ -576,7 +576,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, ScopeReflection_GetUsingNamespaces) { )"; GetAllTopLevelDecls(code, Decls); - std::vector usingNamespaces; + std::vector usingNamespaces; usingNamespaces = Cpp::GetUsingNamespaces( Decls[0]->getASTContext().getTranslationUnitDecl()); @@ -589,7 +589,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, ScopeReflection_GetUsingNamespaces) { )"; GetAllTopLevelDecls(code1, Decls1); - std::vector usingNamespaces1; + std::vector usingNamespaces1; usingNamespaces1 = Cpp::GetUsingNamespaces(Decls1[0]); EXPECT_EQ(usingNamespaces1.size(), 0); } @@ -631,11 +631,11 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, ScopeReflection_GetScope) { TestFixture::CreateInterpreter(); Interp->declare(code); - Cpp::TCppScope_t tu = Cpp::GetScope("", 0); - Cpp::TCppScope_t ns_N = Cpp::GetScope("N", 0); - Cpp::TCppScope_t cl_C = Cpp::GetScope("C", ns_N); - Cpp::TCppScope_t td_T = Cpp::GetScope("T", 0); - Cpp::TCppScope_t non_existent = Cpp::GetScope("sum", 0); + Cpp::DeclRef tu = Cpp::GetScope("", nullptr); + Cpp::DeclRef ns_N = Cpp::GetScope("N", nullptr); + Cpp::DeclRef cl_C = Cpp::GetScope("C", ns_N); + Cpp::DeclRef td_T = Cpp::GetScope("T", nullptr); + Cpp::DeclRef non_existent = Cpp::GetScope("sum", nullptr); EXPECT_EQ(Cpp::GetQualifiedName(tu), ""); EXPECT_EQ(Cpp::GetQualifiedName(ns_N), "N"); @@ -680,10 +680,10 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, ScopeReflection_GetNamed) { TestFixture::CreateInterpreter(interpreter_args); Interp->declare(code); - Cpp::TCppScope_t ns_N1 = Cpp::GetNamed("N1", nullptr); - Cpp::TCppScope_t ns_N2 = Cpp::GetNamed("N2", ns_N1); - Cpp::TCppScope_t cl_C = Cpp::GetNamed("C", ns_N2); - Cpp::TCppScope_t en_E = Cpp::GetNamed("E", cl_C); + Cpp::DeclRef ns_N1 = Cpp::GetNamed("N1", nullptr); + Cpp::DeclRef ns_N2 = Cpp::GetNamed("N2", ns_N1); + Cpp::DeclRef cl_C = Cpp::GetNamed("C", ns_N2); + Cpp::DeclRef en_E = Cpp::GetNamed("E", cl_C); EXPECT_EQ(Cpp::GetQualifiedName(ns_N1), "N1"); EXPECT_EQ(Cpp::GetQualifiedName(ns_N2), "N1::N2"); EXPECT_EQ(Cpp::GetQualifiedName(cl_C), "N1::N2::C"); @@ -696,9 +696,9 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, ScopeReflection_GetNamed) { EXPECT_EQ(Cpp::GetQualifiedName(Cpp::GetNamed("S", cl_C)), "N1::N2::C::S"); Interp->process("#include "); - Cpp::TCppScope_t std_ns = Cpp::GetNamed("std", nullptr); - Cpp::TCppScope_t std_string_class = Cpp::GetNamed("string", std_ns); - Cpp::TCppScope_t std_string_npos_var = Cpp::GetNamed("npos", std_string_class); + Cpp::DeclRef std_ns = Cpp::GetNamed("std", nullptr); + Cpp::DeclRef std_string_class = Cpp::GetNamed("string", std_ns); + Cpp::DeclRef std_string_npos_var = Cpp::GetNamed("npos", std_string_class); EXPECT_EQ(Cpp::GetQualifiedName(std_ns), "std"); EXPECT_EQ(Cpp::GetQualifiedName(std_string_class), "std::string"); EXPECT_EQ(Cpp::GetQualifiedName(std_string_npos_var), "std::basic_string::npos"); @@ -727,16 +727,16 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, ScopeReflection_GetNamedWithUsing) { TestFixture::CreateInterpreter(); Interp->declare(code); - Cpp::TCppScope_t ns_N = Cpp::GetNamed("N"); - Cpp::TCppScope_t ns_M = Cpp::GetNamed("M"); - Cpp::TCppScope_t ns_P = Cpp::GetNamed("P"); + Cpp::DeclRef ns_N = Cpp::GetNamed("N"); + Cpp::DeclRef ns_M = Cpp::GetNamed("M"); + Cpp::DeclRef ns_P = Cpp::GetNamed("P"); ASSERT_TRUE(ns_N); ASSERT_TRUE(ns_M); ASSERT_TRUE(ns_P); - Cpp::TCppScope_t N_C = Cpp::GetNamed("C", ns_N); - Cpp::TCppScope_t N_x = Cpp::GetNamed("x", ns_N); - Cpp::TCppScope_t N_f = Cpp::GetNamed("f", ns_N); + Cpp::DeclRef N_C = Cpp::GetNamed("C", ns_N); + Cpp::DeclRef N_x = Cpp::GetNamed("x", ns_N); + Cpp::DeclRef N_f = Cpp::GetNamed("f", ns_N); ASSERT_TRUE(N_C); ASSERT_TRUE(N_x); ASSERT_TRUE(N_f); @@ -766,7 +766,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, ScopeReflection_GetNamedWithUsing) { namespace P2 { using namespace N; } namespace Q { using namespace P2; } )"); - Cpp::TCppScope_t ns_Q = Cpp::GetNamed("Q"); + Cpp::DeclRef ns_Q = Cpp::GetNamed("Q"); ASSERT_TRUE(ns_Q); EXPECT_EQ(Cpp::GetNamed("C", ns_Q), N_C) << "using namespace P2 (which uses N); GetNamed(\"C\", Q) " @@ -780,7 +780,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, ScopeReflection_GetNamedWithUsing) { namespace N3 { class C {}; } namespace AmbU { using namespace N; using namespace N3; } )"); - Cpp::TCppScope_t ns_Amb = Cpp::GetNamed("AmbU"); + Cpp::DeclRef ns_Amb = Cpp::GetNamed("AmbU"); ASSERT_TRUE(ns_Amb); EXPECT_EQ(Cpp::GetNamed("C", ns_Amb), nullptr) << "ambiguous using-directives must return nullptr, not silently " @@ -810,13 +810,13 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, ScopeReflection_GetParentScope) { TestFixture::CreateInterpreter(); Interp->declare(code); - Cpp::TCppScope_t ns_N1 = Cpp::GetNamed("N1"); - Cpp::TCppScope_t ns_N2 = Cpp::GetNamed("N2", ns_N1); - Cpp::TCppScope_t cl_C = Cpp::GetNamed("C", ns_N2); - Cpp::TCppScope_t int_i = Cpp::GetNamed("i", cl_C); - Cpp::TCppScope_t en_E = Cpp::GetNamed("E", cl_C); - Cpp::TCppScope_t en_A = Cpp::GetNamed("A", cl_C); - Cpp::TCppScope_t en_B = Cpp::GetNamed("B", cl_C); + Cpp::DeclRef ns_N1 = Cpp::GetNamed("N1"); + Cpp::DeclRef ns_N2 = Cpp::GetNamed("N2", ns_N1); + Cpp::DeclRef cl_C = Cpp::GetNamed("C", ns_N2); + Cpp::DeclRef int_i = Cpp::GetNamed("i", cl_C); + Cpp::DeclRef en_E = Cpp::GetNamed("E", cl_C); + Cpp::DeclRef en_A = Cpp::GetNamed("A", cl_C); + Cpp::DeclRef en_B = Cpp::GetNamed("B", cl_C); EXPECT_EQ(Cpp::GetQualifiedName(Cpp::GetParentScope(ns_N1)), ""); EXPECT_EQ(Cpp::GetQualifiedName(Cpp::GetParentScope(ns_N2)), "N1"); @@ -861,8 +861,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, ScopeReflection_GetScopeFromType) { "N::C"); EXPECT_EQ(Cpp::GetQualifiedName(Cpp::GetScopeFromType(QT2.getAsOpaquePtr())), "N::S"); - EXPECT_EQ(Cpp::GetScopeFromType(QT3.getAsOpaquePtr()), - (Cpp::TCppScope_t) 0); + EXPECT_FALSE(Cpp::GetScopeFromType(QT3.getAsOpaquePtr())); EXPECT_EQ(Cpp::GetQualifiedName(Cpp::GetScopeFromType(QT4.getAsOpaquePtr())), "N::C"); EXPECT_EQ(Cpp::GetQualifiedName(Cpp::GetScopeFromType(QT5.getAsOpaquePtr())), @@ -938,16 +937,16 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, ScopeReflection_GetBaseClass) { EXPECT_EQ(get_base_class_name(Decls[4], 0), "D"); EXPECT_EQ(get_base_class_name(Decls[10], 0), ""); - auto* VD = Cpp::GetNamed("var"); - auto *VT = Cpp::GetVariableType(VD); - auto *TC2_A_Decl = Cpp::GetScopeFromType(VT); - auto *TC1_A_Decl = Cpp::GetBaseClass(TC2_A_Decl, 0); + auto VD = Cpp::GetNamed("var"); + auto VT = Cpp::GetVariableType(VD); + auto TC2_A_Decl = Cpp::GetScopeFromType(VT); + auto TC1_A_Decl = Cpp::GetBaseClass(TC2_A_Decl, 0); EXPECT_EQ(Cpp::GetCompleteName(TC1_A_Decl), "TC1"); - auto* VD1 = Cpp::GetNamed("var1"); - auto* VT1 = Cpp::GetVariableType(VD1); - auto* TC3_A_Decl = Cpp::GetScopeFromType(VT1); - auto* A_class = Cpp::GetBaseClass(TC3_A_Decl, 0); + auto VD1 = Cpp::GetNamed("var1"); + auto VT1 = Cpp::GetVariableType(VD1); + auto TC3_A_Decl = Cpp::GetScopeFromType(VT1); + auto A_class = Cpp::GetBaseClass(TC3_A_Decl, 0); EXPECT_EQ(Cpp::GetCompleteName(A_class), "A"); } @@ -1096,8 +1095,8 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, ScopeReflection_InstantiateNNTPClassTemplate) { GetAllTopLevelDecls(code, Decls); ASTContext &C = Interp->getCI()->getASTContext(); - Cpp::TCppType_t IntTy = C.IntTy.getAsOpaquePtr(); - std::vector args1 = {{IntTy, "5"}}; + Cpp::TypeRef IntTy = C.IntTy.getAsOpaquePtr(); + std::vector args1 = {{IntTy.data, "5"}}; EXPECT_TRUE(Cpp::InstantiateTemplate(Decls[0], args1)); } @@ -1111,9 +1110,9 @@ template constexpr T pi = T(3.1415926535897932385L); ASTContext& C = Interp->getCI()->getASTContext(); std::vector args1 = {C.IntTy.getAsOpaquePtr()}; - auto* Instance1 = Cpp::InstantiateTemplate(Decls[0], args1); - EXPECT_TRUE(isa(static_cast(Instance1))); - auto* VD = cast(static_cast(Instance1)); + auto Instance1 = Cpp::InstantiateTemplate(Decls[0], args1); + EXPECT_TRUE(isa(Cpp::unwrap(Instance1))); + auto* VD = cast(Cpp::unwrap(Instance1)); VarTemplateDecl* VDTD1 = VD->getSpecializedTemplate(); EXPECT_TRUE(VDTD1->isThisDeclarationADefinition()); TemplateArgument TA1 = (*VD->getTemplateArgsAsWritten())[0].getArgument(); @@ -1130,9 +1129,9 @@ template T TrivialFnTemplate() { return T(); } ASTContext& C = Interp->getCI()->getASTContext(); std::vector args1 = {C.IntTy.getAsOpaquePtr()}; - auto* Instance1 = Cpp::InstantiateTemplate(Decls[0], args1); - EXPECT_TRUE(isa(static_cast(Instance1))); - FunctionDecl* FD = cast(static_cast(Instance1)); + auto Instance1 = Cpp::InstantiateTemplate(Decls[0], args1); + EXPECT_TRUE(isa(Cpp::unwrap(Instance1))); + FunctionDecl* FD = cast(Cpp::unwrap(Instance1)); FunctionDecl* FnTD1 = FD->getTemplateInstantiationPattern(); EXPECT_TRUE(FnTD1->isThisDeclarationADefinition()); TemplateArgument TA1 = FD->getTemplateSpecializationArgs()->get(0); @@ -1147,7 +1146,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, Interp->process(code); const char* str = "std::make_unique"; auto* Instance1 = - static_cast(Cpp::InstantiateTemplateFunctionFromString(str)); + Cpp::unwrap(Cpp::InstantiateTemplateFunctionFromString(str)); EXPECT_TRUE(Instance1); } @@ -1195,10 +1194,11 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, ScopeReflection_InstantiateTemplate) { ASTContext &C = Interp->getCI()->getASTContext(); std::vector args1 = {C.IntTy.getAsOpaquePtr()}; - auto* Instance1 = Cpp::InstantiateTemplate(Decls[0], args1); + auto Instance1 = Cpp::InstantiateTemplate(Decls[0], args1); EXPECT_TRUE( - isa(static_cast(Instance1))); - auto *CTSD1 = static_cast(Instance1); + isa(Cpp::unwrap(Instance1))); + auto* CTSD1 = + cast(Cpp::unwrap(Instance1)); EXPECT_TRUE(CTSD1->hasDefinition()); TemplateArgument TA1 = CTSD1->getTemplateArgs().get(0); EXPECT_TRUE(TA1.getAsType()->isIntegerType()); @@ -1206,35 +1206,38 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, ScopeReflection_InstantiateTemplate) { auto Instance2 = Cpp::InstantiateTemplate(Decls[1], {}); EXPECT_TRUE( - isa(static_cast(Instance2))); - auto *CTSD2 = static_cast(Instance2); + isa(Cpp::unwrap(Instance2))); + auto* CTSD2 = + cast(Cpp::unwrap(Instance2)); EXPECT_TRUE(CTSD2->hasDefinition()); TemplateArgument TA2 = CTSD2->getTemplateArgs().get(0); EXPECT_TRUE(TA2.getAsType()->isIntegerType()); std::vector args3 = {C.IntTy.getAsOpaquePtr()}; - auto* Instance3 = Cpp::InstantiateTemplate(Decls[2], args3); + auto Instance3 = Cpp::InstantiateTemplate(Decls[2], args3); EXPECT_TRUE( - isa(static_cast(Instance3))); - auto *CTSD3 = static_cast(Instance3); + isa(Cpp::unwrap(Instance3))); + auto* CTSD3 = + cast(Cpp::unwrap(Instance3)); EXPECT_TRUE(CTSD3->hasDefinition()); TemplateArgument TA3_0 = CTSD3->getTemplateArgs().get(0); TemplateArgument TA3_1 = CTSD3->getTemplateArgs().get(1); EXPECT_TRUE(TA3_0.getAsType()->isIntegerType()); EXPECT_TRUE(Cpp::IsRecordType(TA3_1.getAsType().getAsOpaquePtr())); - std::vector Inst3_methods; + std::vector Inst3_methods; Cpp::GetClassMethods(Instance3, Inst3_methods); EXPECT_EQ(Cpp::GetFunctionSignature(Inst3_methods[0]), "C1::C1(const C0 &val)"); std::vector args4 = {C.IntTy.getAsOpaquePtr(), {C.IntTy.getAsOpaquePtr(), "3"}}; - auto* Instance4 = Cpp::InstantiateTemplate(Decls[3], args4); + auto Instance4 = Cpp::InstantiateTemplate(Decls[3], args4); EXPECT_TRUE( - isa(static_cast(Instance4))); - auto *CTSD4 = static_cast(Instance4); + isa(Cpp::unwrap(Instance4))); + auto* CTSD4 = + cast(Cpp::unwrap(Instance4)); EXPECT_TRUE(CTSD4->hasDefinition()); TemplateArgument TA4_0 = CTSD4->getTemplateArgs().get(0); TemplateArgument TA4_1 = CTSD4->getTemplateArgs().get(1); @@ -1256,8 +1259,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, ScopeReflection_GetClassTemplateArgs) { GetAllTopLevelDecls(code, Decls); - Cpp::TCppScope_t f = - Cpp::GetScopeFromType(Cpp::GetVariableType(Decls.back())); + Cpp::DeclRef f = Cpp::GetScopeFromType(Cpp::GetVariableType(Decls.back())); EXPECT_TRUE(f); std::vector tmpl_args; @@ -1286,14 +1288,14 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, GetAllTopLevelDecls(code, Decls); - auto* v1 = Cpp::GetNamed("v1"); - auto* v2 = Cpp::GetNamed("v2"); - auto* v3 = Cpp::GetNamed("v3"); + auto v1 = Cpp::GetNamed("v1"); + auto v2 = Cpp::GetNamed("v2"); + auto v3 = Cpp::GetNamed("v3"); EXPECT_TRUE(v1 && v2 && v3); - auto *v1_class = Cpp::GetScopeFromType(Cpp::GetVariableType(v1)); - auto *v2_class = Cpp::GetScopeFromType(Cpp::GetVariableType(v2)); - auto *v3_class = Cpp::GetScopeFromType(Cpp::GetVariableType(v3)); + auto v1_class = Cpp::GetScopeFromType(Cpp::GetVariableType(v1)); + auto v2_class = Cpp::GetScopeFromType(Cpp::GetVariableType(v2)); + auto v3_class = Cpp::GetScopeFromType(Cpp::GetVariableType(v3)); EXPECT_TRUE(v1_class && v2_class && v3_class); std::vector instance_types; @@ -1360,7 +1362,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, ScopeReflection_GetOperator) { Cpp::Declare(code.c_str()); - std::vector ops; + std::vector ops; Cpp::GetOperator(Cpp::GetGlobalScope(), Cpp::Operator::OP_Plus, ops); EXPECT_EQ(ops.size(), 1); diff --git a/interpreter/CppInterOp/unittests/CppInterOp/TestDownstreamLib/TestDownstreamLib.cpp b/interpreter/CppInterOp/unittests/CppInterOp/TestDownstreamLib/TestDownstreamLib.cpp index e27449e042a27..6c2123ef91772 100644 --- a/interpreter/CppInterOp/unittests/CppInterOp/TestDownstreamLib/TestDownstreamLib.cpp +++ b/interpreter/CppInterOp/unittests/CppInterOp/TestDownstreamLib/TestDownstreamLib.cpp @@ -14,14 +14,14 @@ namespace DispatchRaw { // ODR-uses the inline JitCall fast path. JC is opaque so the optimizer // can't DCE the calls at any -O level. The body never runs at test // time; only the .o's UND-symbol surface matters. -void downstream_link_probe(CppImpl::JitCall* JC) { +void downstream_link_probe(Cpp::JitCall* JC) { JC->Invoke(); JC->InvokeConstructor(nullptr); JC->InvokeDestructor(nullptr); } int downstream_verify_trace_slots(const char* libpath) { - if (!CppInternal::Dispatch::LoadDispatchAPI(libpath)) + if (!Cpp::LoadDispatchAPI(libpath)) return 1; if (!CppInternal::DispatchRaw::CppInterOpTraceJitCallInvokeImpl) return 2; diff --git a/interpreter/CppInterOp/unittests/CppInterOp/TestDownstreamLib/TestDownstreamLib.h b/interpreter/CppInterOp/unittests/CppInterOp/TestDownstreamLib/TestDownstreamLib.h index e84d1dbdab42c..b47e459d02399 100644 --- a/interpreter/CppInterOp/unittests/CppInterOp/TestDownstreamLib/TestDownstreamLib.h +++ b/interpreter/CppInterOp/unittests/CppInterOp/TestDownstreamLib/TestDownstreamLib.h @@ -3,7 +3,7 @@ // Probe entry that ODR-uses the inline JitCall fast path. JC is opaque // so the optimizer can't DCE the inlined references at any -O level. -namespace CppImpl { +namespace Cpp { class JitCall; } #ifdef _WIN32 @@ -11,7 +11,7 @@ class JitCall; #else #define TESTDOWNSTREAM_EXPORT extern "C" __attribute__((visibility("default"))) #endif -TESTDOWNSTREAM_EXPORT void downstream_link_probe(CppImpl::JitCall* JC); +TESTDOWNSTREAM_EXPORT void downstream_link_probe(Cpp::JitCall* JC); /// After LoadDispatchAPI(libpath) succeeds, check every DispatchRaw /// trace slot in this DSO is non-null. Returns 0 on success, a diff --git a/interpreter/CppInterOp/unittests/CppInterOp/TestSharedLib/TestSharedLib.cpp b/interpreter/CppInterOp/unittests/CppInterOp/TestSharedLib/TestSharedLib.cpp index c0d7c474c00a2..45dbe9dd8b532 100644 --- a/interpreter/CppInterOp/unittests/CppInterOp/TestSharedLib/TestSharedLib.cpp +++ b/interpreter/CppInterOp/unittests/CppInterOp/TestSharedLib/TestSharedLib.cpp @@ -1,3 +1,9 @@ #include "TestSharedLib.h" int ret_zero() { return 0; } + +OverlayBase::OverlayBase() {} +OverlayBase::~OverlayBase() {} +int OverlayBase::frob(int x) { return x; } + +int OverlayDispatchOnce(OverlayBase* b, int x) { return b->frob(x); } diff --git a/interpreter/CppInterOp/unittests/CppInterOp/TestSharedLib/TestSharedLib.h b/interpreter/CppInterOp/unittests/CppInterOp/TestSharedLib/TestSharedLib.h index a56ba42293981..6b32130a964c6 100644 --- a/interpreter/CppInterOp/unittests/CppInterOp/TestSharedLib/TestSharedLib.h +++ b/interpreter/CppInterOp/unittests/CppInterOp/TestSharedLib/TestSharedLib.h @@ -1,11 +1,26 @@ #ifndef UNITTESTS_CPPINTEROP_TESTSHAREDLIB_TESTSHAREDLIB_H #define UNITTESTS_CPPINTEROP_TESTSHAREDLIB_TESTSHAREDLIB_H -// Avoid having to mangle/demangle the symbol name in tests #ifdef _WIN32 -extern "C" __declspec(dllexport) int ret_zero(); +#define TESTSHAREDLIB_API __declspec(dllexport) #else -extern "C" int __attribute__((visibility("default"))) ret_zero(); +#define TESTSHAREDLIB_API __attribute__((visibility("default"))) #endif +// Avoid having to mangle/demangle the symbol name in tests +extern "C" TESTSHAREDLIB_API int ret_zero(); + +// A polymorphic type whose vtable is anchored in this shared library, +// plus a dispatch helper compiled here. Calling OverlayDispatchOnce +// from another translation unit is a genuine cross-DSO virtual call +// the caller's compiler cannot devirtualize or inline -- the honest +// setting for measuring VTableOverlay dispatch cost. +struct TESTSHAREDLIB_API OverlayBase { + OverlayBase(); + virtual ~OverlayBase(); + virtual int frob(int x); +}; + +extern "C" TESTSHAREDLIB_API int OverlayDispatchOnce(OverlayBase* b, int x); + #endif // UNITTESTS_CPPINTEROP_TESTSHAREDLIB_TESTSHAREDLIB_H diff --git a/interpreter/CppInterOp/unittests/CppInterOp/TracingTests.cpp b/interpreter/CppInterOp/unittests/CppInterOp/TracingTests.cpp index e3f1122f43082..e5be411daaaa4 100644 --- a/interpreter/CppInterOp/unittests/CppInterOp/TracingTests.cpp +++ b/interpreter/CppInterOp/unittests/CppInterOp/TracingTests.cpp @@ -140,7 +140,7 @@ TEST_F(TracingTest, CountParamsHandlesVoidParamList) { using TR = CppInterOp::Tracing::TraceRegion; EXPECT_EQ(TR::countParams("void foo()"), 0U); EXPECT_EQ(TR::countParams("void foo(void)"), 0U); - EXPECT_EQ(TR::countParams("void *__cdecl CppImpl::GetInterpreter(void)"), 0U); + EXPECT_EQ(TR::countParams("void *__cdecl Cpp::GetInterpreter(void)"), 0U); EXPECT_EQ(TR::countParams("void foo(int)"), 1U); EXPECT_EQ(TR::countParams("void foo(int, double)"), 2U); EXPECT_EQ(TR::countParams("void foo(std::vector&)"), 1U); @@ -1224,14 +1224,13 @@ TEST_F(TracingTest, JitCallWrapperSourceLogged) { // Placement new requires . Cpp::Declare("#include "); Cpp::Declare("namespace WrapNS { int add(int a, int b) { return a + b; } }"); - auto* Func = - static_cast(Cpp::GetNamed("add", Cpp::GetScope("WrapNS"))); - ASSERT_NE(Func, nullptr); + auto Func = Cpp::GetNamed("add", Cpp::GetScope("WrapNS")); + ASSERT_TRUE(Func); TheTraceInfo->clear(); // MakeFunctionCallable triggers make_wrapper which logs the wrapper source. - auto JC = Cpp::MakeFunctionCallable(Func); + auto JC = Cpp::MakeFunctionCallable(Cpp::FuncRef{Func.data}); ASSERT_TRUE(JC.isValid()); auto log = getFullLog(); @@ -1248,11 +1247,10 @@ TEST_F(TracingTest, JitCallInvokeLogged) { Cpp::Declare("#include "); Cpp::Declare("namespace InvNS { int square(int x) { return x * x; } }"); - auto* Func = - static_cast(Cpp::GetNamed("square", Cpp::GetScope("InvNS"))); - ASSERT_NE(Func, nullptr); + auto Func = Cpp::GetNamed("square", Cpp::GetScope("InvNS")); + ASSERT_TRUE(Func); - auto JC = Cpp::MakeFunctionCallable(Func); + auto JC = Cpp::MakeFunctionCallable(Cpp::FuncRef{Func.data}); ASSERT_TRUE(JC.isValid()); // Clear so only the Invoke shows up. @@ -1281,15 +1279,15 @@ TEST_F(TracingTest, JitCallConstructorInvokeLogged) { Cpp::Declare("#include "); Cpp::Declare("namespace InvCtorNS { struct Bar { int x = 7; }; }"); - auto* ClassBar = Cpp::GetScope("Bar", Cpp::GetScope("InvCtorNS")); - ASSERT_NE(ClassBar, nullptr); - auto* CtorD = Cpp::GetDefaultConstructor(ClassBar); - ASSERT_NE(CtorD, nullptr); + auto ClassBar = Cpp::GetScope("Bar", Cpp::GetScope("InvCtorNS")); + ASSERT_TRUE(ClassBar); + auto CtorD = Cpp::GetDefaultConstructor(ClassBar); + ASSERT_TRUE(CtorD); // Resolve the dtor up-front so the cleanup at the end of the test // can free the heap-allocated Bar (LeakSanitizer otherwise flags // the 4-byte `int x` payload). - auto* DtorD = Cpp::GetDestructor(ClassBar); - ASSERT_NE(DtorD, nullptr); + auto DtorD = Cpp::GetDestructor(ClassBar); + ASSERT_TRUE(DtorD); auto CtorJC = Cpp::MakeFunctionCallable(CtorD); auto DtorJC = Cpp::MakeFunctionCallable(DtorD); @@ -1316,12 +1314,12 @@ TEST_F(TracingTest, JitCallDestructorInvokeLogged) { Cpp::Declare("#include "); Cpp::Declare("namespace InvDtorNS { struct Baz { ~Baz() {} }; }"); - auto* ClassBaz = Cpp::GetScope("Baz", Cpp::GetScope("InvDtorNS")); - ASSERT_NE(ClassBaz, nullptr); - auto* CtorD = Cpp::GetDefaultConstructor(ClassBaz); - ASSERT_NE(CtorD, nullptr); - auto* DtorD = Cpp::GetDestructor(ClassBaz); - ASSERT_NE(DtorD, nullptr); + auto ClassBaz = Cpp::GetScope("Baz", Cpp::GetScope("InvDtorNS")); + ASSERT_TRUE(ClassBaz); + auto CtorD = Cpp::GetDefaultConstructor(ClassBaz); + ASSERT_TRUE(CtorD); + auto DtorD = Cpp::GetDestructor(ClassBaz); + ASSERT_TRUE(DtorD); auto CtorJC = Cpp::MakeFunctionCallable(CtorD); auto DtorJC = Cpp::MakeFunctionCallable(DtorD); @@ -1351,9 +1349,9 @@ TEST_F(TracingTest, JitCallReturnedPointerRegistered) { Cpp::Declare("#include "); Cpp::Declare("namespace InvRetNS { struct Foo {}; " "Foo* makeFoo() { return new Foo(); } }"); - auto* MakeFD = Cpp::GetNamed("makeFoo", Cpp::GetScope("InvRetNS")); - ASSERT_NE(MakeFD, nullptr); - auto MakeJC = Cpp::MakeFunctionCallable(MakeFD); + auto MakeFD = Cpp::GetNamed("makeFoo", Cpp::GetScope("InvRetNS")); + ASSERT_TRUE(MakeFD); + auto MakeJC = Cpp::MakeFunctionCallable(Cpp::FuncRef{MakeFD.data}); ASSERT_TRUE(MakeJC.isValid()); // Sanity: an unrelated pointer remains unregistered. @@ -1589,9 +1587,11 @@ TEST(TracingCoverageTest, AllPublicAPIsAreTraced) { size_t Pos = 0; bool Found = false; while ((Pos = Impl.find(Pattern, Pos)) != std::string::npos) { - // Find the opening brace of the function body. + // Find the opening brace of the function body. Wide-signature window + // (500) accommodates 8-parameter APIs like MakeVTableOverlay laid out + // one-arg-per-line LLVM-style. size_t BracePos = Impl.find('{', Pos); - if (BracePos == std::string::npos || BracePos - Pos > 300) { + if (BracePos == std::string::npos || BracePos - Pos > 500) { Pos += Pattern.size(); continue; } diff --git a/interpreter/CppInterOp/unittests/CppInterOp/TypeReflectionTest.cpp b/interpreter/CppInterOp/unittests/CppInterOp/TypeReflectionTest.cpp index 6df0a6c6f595b..3fa88696c7d71 100644 --- a/interpreter/CppInterOp/unittests/CppInterOp/TypeReflectionTest.cpp +++ b/interpreter/CppInterOp/unittests/CppInterOp/TypeReflectionTest.cpp @@ -144,7 +144,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, TypeReflection_GetTypeWithParent) { } )"); - Cpp::TCppScope_t ns = Cpp::GetNamed("NS"); + Cpp::DeclRef ns = Cpp::GetNamed("NS"); ASSERT_TRUE(ns); // Builtin fast-path is independent of parent: `int` resolves @@ -443,11 +443,11 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, TypeReflection_IsTypeDerivedFrom) { GetAllTopLevelDecls(code, Decls); - Cpp::TCppType_t type_A = Cpp::GetVariableType(Decls[5]); - Cpp::TCppType_t type_B = Cpp::GetVariableType(Decls[6]); - Cpp::TCppType_t type_C = Cpp::GetVariableType(Decls[7]); - Cpp::TCppType_t type_D = Cpp::GetVariableType(Decls[8]); - Cpp::TCppType_t type_E = Cpp::GetVariableType(Decls[9]); + Cpp::TypeRef type_A = Cpp::GetVariableType(Decls[5]); + Cpp::TypeRef type_B = Cpp::GetVariableType(Decls[6]); + Cpp::TypeRef type_C = Cpp::GetVariableType(Decls[7]); + Cpp::TypeRef type_D = Cpp::GetVariableType(Decls[8]); + Cpp::TypeRef type_E = Cpp::GetVariableType(Decls[9]); EXPECT_TRUE(Cpp::IsTypeDerivedFrom(type_B, type_A)); EXPECT_TRUE(Cpp::IsTypeDerivedFrom(type_D, type_B)); @@ -575,7 +575,21 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, TypeReflection_IsPODType) { GetAllTopLevelDecls(code, Decls); EXPECT_TRUE(Cpp::IsPODType(Cpp::GetVariableType(Decls[2]))); EXPECT_FALSE(Cpp::IsPODType(Cpp::GetVariableType(Decls[3]))); - EXPECT_FALSE(Cpp::IsPODType(0)); + EXPECT_FALSE(Cpp::IsPODType(nullptr)); +} + +TYPED_TEST(CPPINTEROP_TEST_MODE, TypeReflection_IsTemplateParmType) { + std::vector Decls; + + std::string code = R"( + template void f(T t, const T& r, int i) {} + )"; + + GetAllTopLevelDecls(code, Decls); + + EXPECT_TRUE(Cpp::IsTemplateParmType(Cpp::GetFunctionArgType(Decls[0], 0))); + EXPECT_FALSE(Cpp::IsTemplateParmType(Cpp::GetFunctionArgType(Decls[0], 1))); + EXPECT_FALSE(Cpp::IsTemplateParmType(Cpp::GetFunctionArgType(Decls[0], 2))); } TYPED_TEST(CPPINTEROP_TEST_MODE, TypeReflection_IsSmartPtrType) { @@ -659,14 +673,14 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, TypeReflection_TypeQualifiers) { int *__restrict__ const volatile h = nullptr; )"); - Cpp::TCppType_t a = Cpp::GetVariableType(Cpp::GetNamed("a")); - Cpp::TCppType_t b = Cpp::GetVariableType(Cpp::GetNamed("b")); - Cpp::TCppType_t c = Cpp::GetVariableType(Cpp::GetNamed("c")); - Cpp::TCppType_t d = Cpp::GetVariableType(Cpp::GetNamed("d")); - Cpp::TCppType_t e = Cpp::GetVariableType(Cpp::GetNamed("e")); - Cpp::TCppType_t f = Cpp::GetVariableType(Cpp::GetNamed("f")); - Cpp::TCppType_t g = Cpp::GetVariableType(Cpp::GetNamed("g")); - Cpp::TCppType_t h = Cpp::GetVariableType(Cpp::GetNamed("h")); + Cpp::TypeRef a = Cpp::GetVariableType(Cpp::GetNamed("a")); + Cpp::TypeRef b = Cpp::GetVariableType(Cpp::GetNamed("b")); + Cpp::TypeRef c = Cpp::GetVariableType(Cpp::GetNamed("c")); + Cpp::TypeRef d = Cpp::GetVariableType(Cpp::GetNamed("d")); + Cpp::TypeRef e = Cpp::GetVariableType(Cpp::GetNamed("e")); + Cpp::TypeRef f = Cpp::GetVariableType(Cpp::GetNamed("f")); + Cpp::TypeRef g = Cpp::GetVariableType(Cpp::GetNamed("g")); + Cpp::TypeRef h = Cpp::GetVariableType(Cpp::GetNamed("h")); EXPECT_FALSE(Cpp::HasTypeQualifier(nullptr, Cpp::QualKind::Const)); EXPECT_FALSE(Cpp::RemoveTypeQualifier(nullptr, Cpp::QualKind::Const)); @@ -769,7 +783,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, TypeReflection_IsFloatingType) { EXPECT_TRUE(Cpp::IsFloatingType(Cpp::GetVariableType(Decls[2]))); EXPECT_FALSE(Cpp::IsFloatingType(Cpp::GetVariableType(Decls[3]))); EXPECT_FALSE(Cpp::IsFloatingType(Cpp::GetVariableType(Decls[4]))); - EXPECT_FALSE(Cpp::IsFloatingType(0)); + EXPECT_FALSE(Cpp::IsFloatingType(nullptr)); } TYPED_TEST(CPPINTEROP_TEST_MODE, TypeReflection_IsVoidPointerType) { diff --git a/interpreter/CppInterOp/unittests/CppInterOp/Utils.h b/interpreter/CppInterOp/unittests/CppInterOp/Utils.h index 1101b6f46a303..fba5915e21160 100644 --- a/interpreter/CppInterOp/unittests/CppInterOp/Utils.h +++ b/interpreter/CppInterOp/unittests/CppInterOp/Utils.h @@ -2,10 +2,12 @@ #define CPPINTEROP_UNITTESTS_LIBCPPINTEROP_UTILS_H #include "../../lib/CppInterOp/Compatibility.h" +#include "../../lib/CppInterOp/Unwrap.h" #include "CppInterOp/CppInterOp.h" #define CPPINTEROP_TEST_MODE CppInterOpTest +#include #include #include #include @@ -18,9 +20,21 @@ using namespace llvm; namespace clang { class Decl; } -#define Interp (static_cast(Cpp::GetInterpreter())) +#define Interp (Cpp::unwrap(Cpp::GetInterpreter())) namespace TestUtils { +// Function-pointer / void* round-trip for vtable test/bench code, mirroring +// the lib-side VTableOverlay::BitCastFn. Sidesteps the conditionally- +// supported reinterpret_cast between fn and data pointers per +// [expr.reinterpret.cast]/6; memcpy is well-defined on every platform +// CppInterOp targets. +template inline To BitCastFn(From f) noexcept { + static_assert(sizeof(To) == sizeof(From)); + To to; + std::memcpy(&to, &f, sizeof(to)); + return to; +} + struct TestConfig { std::string name; bool use_oop_jit; @@ -97,7 +111,7 @@ template class CPPINTEROP_TEST_MODE : public ::testing::Test { } public: - static Cpp::TInterp_t + static Cpp::InterpRef CreateInterpreter(const std::vector& Args = {}, const std::vector& GpuArgs = {}) { auto mergedArgs = TestUtils::GetInterpreterArgs(Args); diff --git a/interpreter/CppInterOp/unittests/CppInterOp/VTableOverlayCrossTUBench.cpp b/interpreter/CppInterOp/unittests/CppInterOp/VTableOverlayCrossTUBench.cpp new file mode 100644 index 0000000000000..3d839dc6ede65 --- /dev/null +++ b/interpreter/CppInterOp/unittests/CppInterOp/VTableOverlayCrossTUBench.cpp @@ -0,0 +1,226 @@ +// Cross-TU performance check for VTableOverlay. +// +// The dispatch loop (OverlayDispatchOnce) lives in TestSharedLib, so the +// call site cannot devirtualize or inline against the derived type known +// here. That makes the measurement honest: it reflects a real AOT caller +// in another library dispatching through the (possibly overlaid) vtable. +// +// VTableOverlay does NOT make the call faster -- it is not devirtualization +// or inlining; it only swaps the function pointer installed at a vtable +// slot. The claim under test is that the swap is *free at the call site*: +// the overlaid path runs the same vtable indirection as a plain virtual +// call, so it costs the same. That zero-overhead property is what enables +// language bindings to redirect dispatch into Python (or another runtime) +// without paying any per-call cost over the C++ virtual baseline. + +#include "CppInterOp/CppInterOp.h" +#include "CppInterOp/CppInterOpTypes.h" +#include "TestSharedLib/TestSharedLib.h" + +#include "Utils.h" +#include "PerfCompare.h" +#include "gtest/gtest.h" + +#include + +#include +#include +#include + +namespace { + +struct Impl : OverlayBase { + [[gnu::noinline]] int frob(int x) override { return x + 1; } +}; +extern "C" int xtu_replacement(void* /*self*/, int x) { return x + 100; } + +// Reflect OverlayBase (definition mirrors TestSharedLib) so the overlay is +// driven through the public reflected API -- the language-binding setting. +Cpp::DeclRef ReflectOverlayBase() { + Cpp::CreateInterpreter({}); + Cpp::Declare("struct OverlayBase {" + " OverlayBase();" + " virtual ~OverlayBase();" + " virtual int frob(int x);" + "};"); + return Cpp::GetNamed("OverlayBase"); +} + +Cpp::FuncRef Frob(Cpp::DeclRef scope) { + std::vector methods; + Cpp::GetClassMethods(scope, methods); + for (auto m : methods) + if (Cpp::GetName(Cpp::DeclRef{m.data}) == "frob") + return m; + return nullptr; +} + +Cpp::UniqueVTableOverlay OverlayFrob(void* inst, Cpp::DeclRef base) { + return Cpp::MakeUniqueVTableOverlay( + inst, base, {{Frob(base), TestUtils::BitCastFn(&xtu_replacement)}}); +} + +} // namespace + +TEST(VTableOverlayCrossTU, OverlayDispatchesAcrossDSO) { + Impl impl; + auto base = ReflectOverlayBase(); + ASSERT_NE(base, nullptr); + auto ov = OverlayFrob(&impl, base); + ASSERT_TRUE(ov); + // The call site is in TestSharedLib; the overlay still takes effect. + EXPECT_EQ(OverlayDispatchOnce(&impl, 7), 107); +} + +static void BM_XTU_BareVirtual(benchmark::State& state) { + Impl impl; + for (auto _ : state) + benchmark::DoNotOptimize(OverlayDispatchOnce(&impl, 7)); +} +BENCHMARK(BM_XTU_BareVirtual); + +static void BM_XTU_OverlayDirectFn(benchmark::State& state) { + Impl impl; + auto base = ReflectOverlayBase(); + auto ov = OverlayFrob(&impl, base); + for (auto _ : state) + benchmark::DoNotOptimize(OverlayDispatchOnce(&impl, 7)); +} +BENCHMARK(BM_XTU_OverlayDirectFn); + +TEST(VTableOverlayCrossTU, OverlayAddsNoPerCallCost) { +#if !defined(NDEBUG) || defined(__SANITIZE_ADDRESS__) + GTEST_SKIP() << "Perf assertions need a Release, non-sanitizer build."; +#endif +#ifdef __APPLE__ + GTEST_SKIP() << "Flaky on macOS runners (ratio noise around the 0.9 floor)."; +#endif + EXPECT_NOT_SLOWER_THAN(BM_XTU_OverlayDirectFn, BM_XTU_BareVirtual); +} + +// Pin the perf claim that motivates n_extra_prefix_slots: a thunk +// reading per-instance state via the extra slot is measurably faster +// than the alternative bindings would otherwise reach for (a +// process-global pointer registry keyed by `self`). The dispatched +// work is identical in both thunks -- only the state-lookup path +// differs -- so the gap measured here is the lookup itself. +namespace { +struct Handler { + int v; + [[gnu::noinline]] int add(int x) { return v + x; } +}; + +extern "C" int xtu_thunk_extra_slot(void* self, int x) { + auto* h = static_cast(Cpp::VTableOverlayExtraSlot(self, 0)); + return h->add(x); +} + +// Bench-only stand-in for the naive alternative. Production code does +// not pay a global mutex per dispatch -- that is the contrast the +// EXPECT_AT_LEAST_N_TIMES_FASTER assertion below pins down. +std::unordered_map NaiveHandlerMap; +std::mutex NaiveHandlerMapMutex; + +extern "C" int xtu_thunk_global_map(void* self, int x) { + Handler* h; + { + std::lock_guard lk(NaiveHandlerMapMutex); + h = NaiveHandlerMap[self]; + } + return h->add(x); +} + +Handler g_handler{100}; +} // namespace + +TEST(VTableOverlayCrossTU, ExtraPrefixSlotDispatch) { + Impl impl; + auto base = ReflectOverlayBase(); + ASSERT_NE(base, nullptr); + auto ov = Cpp::MakeUniqueVTableOverlay( + &impl, base, + {{Frob(base), TestUtils::BitCastFn(&xtu_thunk_extra_slot)}}, + /*n_extra_prefix_slots=*/1); + ASSERT_TRUE(ov); + Cpp::VTableOverlayExtraSlot(&impl, 0) = &g_handler; + EXPECT_EQ(OverlayDispatchOnce(&impl, 7), g_handler.v + 7); +} + +static void BM_XTU_OverlayExtraSlot(benchmark::State& state) { + Impl impl; + auto base = ReflectOverlayBase(); + auto ov = Cpp::MakeUniqueVTableOverlay( + &impl, base, + {{Frob(base), TestUtils::BitCastFn(&xtu_thunk_extra_slot)}}, + /*n_extra_prefix_slots=*/1); + Cpp::VTableOverlayExtraSlot(&impl, 0) = &g_handler; + for (auto _ : state) + benchmark::DoNotOptimize(OverlayDispatchOnce(&impl, 7)); +} +BENCHMARK(BM_XTU_OverlayExtraSlot); + +static void BM_XTU_OverlayGlobalMap(benchmark::State& state) { + Impl impl; + auto base = ReflectOverlayBase(); + auto ov = Cpp::MakeUniqueVTableOverlay( + &impl, base, + {{Frob(base), TestUtils::BitCastFn(&xtu_thunk_global_map)}}); + { + std::lock_guard lk(NaiveHandlerMapMutex); + NaiveHandlerMap[&impl] = &g_handler; + } + for (auto _ : state) + benchmark::DoNotOptimize(OverlayDispatchOnce(&impl, 7)); + { + std::lock_guard lk(NaiveHandlerMapMutex); + NaiveHandlerMap.erase(&impl); + } +} +BENCHMARK(BM_XTU_OverlayGlobalMap); + +TEST(VTableOverlayCrossTU, ExtraSlotBeatsGlobalMap) { +#if !defined(NDEBUG) || defined(__SANITIZE_ADDRESS__) + GTEST_SKIP() << "Perf assertions need a Release, non-sanitizer build."; +#endif + // The map path is hash + mutex per dispatch; the slot path is one mov. + // Even with a fast hash the gap is real; require >= 2x to ride out jitter. + EXPECT_AT_LEAST_N_TIMES_FASTER(BM_XTU_OverlayExtraSlot, + BM_XTU_OverlayGlobalMap, 2.0); +} + +// Confirms on_destroy is destruction-only: dispatch through any other +// slot pays nothing. Reported for trend visibility, with no +// EXPECT_*_FASTER assertion: the dtor wrapper patches the deleting-dtor +// slot, not the dispatched method's slot, so the per-call cost is +// structurally unchanged (see MakeVTableOverlay). The ~30% run-to-run +// jitter on sub-10 ns measurements on CI exceeds any tolerance that +// would still catch a real regression, so a relative assertion here is +// noise, not signal. +extern "C" void xtu_noop_cleanup(void* /*inst*/, void* /*data*/) {} + +static void BM_XTU_OverlayWithDtorHook(benchmark::State& state) { + Impl impl; + auto base = ReflectOverlayBase(); + void* fn = TestUtils::BitCastFn(&xtu_replacement); + Cpp::ConstFuncRef method = Frob(base); + auto* ov = Cpp::MakeVTableOverlay( + &impl, base, &method, &fn, /*n=*/1, + /*n_extra_prefix_slots=*/0, + /*on_destroy=*/&xtu_noop_cleanup, + /*cleanup_data=*/nullptr); + for (auto _ : state) + benchmark::DoNotOptimize(OverlayDispatchOnce(&impl, 7)); + Cpp::DestroyVTableOverlay(ov); +} +BENCHMARK(BM_XTU_OverlayWithDtorHook); + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + benchmark::Initialize(&argc, argv); + // Print formatted benchmark output whenever the perf-assertion tests + // themselves would run -- Debug / sanitizer numbers are noise. +#if defined(NDEBUG) && !defined(__SANITIZE_ADDRESS__) + benchmark::RunSpecifiedBenchmarks(); +#endif + return RUN_ALL_TESTS(); +} diff --git a/interpreter/CppInterOp/unittests/CppInterOp/VTableOverlayTest.cpp b/interpreter/CppInterOp/unittests/CppInterOp/VTableOverlayTest.cpp new file mode 100644 index 0000000000000..a4a41ab0dc893 --- /dev/null +++ b/interpreter/CppInterOp/unittests/CppInterOp/VTableOverlayTest.cpp @@ -0,0 +1,594 @@ +#include "CppInterOp/CppInterOp.h" +#include "CppInterOp/CppInterOpTypes.h" + +#include "Utils.h" +#include "gtest/gtest.h" + +#include +#include + +namespace { + +// Replacement functions installed into vtable slots. The leading self +// parameter matches the implicit `this` of the virtual they replace. +extern "C" int repl_negate(void* /*self*/, int x) { return -x; } +extern "C" int repl_double(void* /*self*/, int x) { return x * 2; } + +// Replacement that reads the object via `this`: layout is { vptr, int value } +// on every ABI we target, so the data member sits at sizeof(void*). +extern "C" int repl_read_value(void* self, int x) { + int v = *reinterpret_cast(static_cast(self) + sizeof(void*)); + return v + x; +} + +// Parallel test-TU definitions of A / B used by the OverlayThroughHierarchy +// test. Layout and ABI come from the host compiler here; the same struct +// shape is declared at interpreter level inside the test so reflection +// computes matching vtable slot indices. foo / bar access the object's +// data members via typed static_cast on the self pointer the vtable slot +// hands them -- exactly the pattern a language binding's adapter writes. +// Virtual destructors keep the vtable layout in line with the other tests +// here (Itanium D1/D0 pair before user virtuals; MSVC single deleting-dtor). +struct A { + int m_x; + A(int x) : m_x(x) {} + virtual ~A() {} + virtual int method() { return m_x; } +}; + +struct B : A { + double m_d; + B(int x, double d) : A(x), m_d(d) {} + int method() override { return static_cast(m_d); } +}; + +extern "C" int foo(void* self) { + return static_cast(self)->m_x + 10; +} +extern "C" int bar(void* self) { + return static_cast(static_cast(self)->m_x + + static_cast(self)->m_d); +} + +int call_slot_no_arg(void* inst, int slot) { + void** vptr = *reinterpret_cast(inst); + return TestUtils::BitCastFn(vptr[slot])(inst); +} + +// OverlayB declared through the interpreter so the overlay runs against +// a reflected, JIT-constructed object -- the language-binding setting. +// Itanium has a destructor pair (D1/D0) before the user virtuals; MSVC +// has a single deleting-dtor slot, so the user-virtual indices differ. +Cpp::DeclRef DeclareBase() { + // -include new: Construct's wrapper uses placement new, so every + // interpreter compilation must see . + Cpp::CreateInterpreter({"-include", "new"}); + Cpp::Declare("struct OverlayB {" + " virtual ~OverlayB() {}" + " virtual int alpha(int x) { return x + 10; }" + " virtual int beta(int x) { return x + 20; }" + "};"); + return Cpp::GetNamed("OverlayB"); +} + +Cpp::FuncRef Method(Cpp::DeclRef scope, const char* name) { + std::vector methods; + Cpp::GetClassMethods(scope, methods); + for (auto m : methods) + if (Cpp::GetName(Cpp::DeclRef{m.data}) == name) + return m; + return nullptr; +} + +// Dispatch through the installed vtable slot. Calling beta() directly in +// this TU would let the compiler devirtualize and bypass the overlay; +// reading the slot tests what the overlay actually installs. +int call_slot(void* inst, int slot, int arg) { + void** vptr = *reinterpret_cast(inst); + return TestUtils::BitCastFn(vptr[slot])(inst, arg); +} + +#ifdef _WIN32 +constexpr int kAlpha = 1; +constexpr int kBeta = 2; +#else +constexpr int kAlpha = 2; +constexpr int kBeta = 3; +#endif + +} // namespace + +TEST(VTableOverlay, ReplacesSlotPreservingOthers) { + auto B = DeclareBase(); + void* inst = Cpp::Construct(B).data; + ASSERT_NE(inst, nullptr); + auto ov = Cpp::MakeUniqueVTableOverlay( + inst, B, {{Method(B, "beta"), TestUtils::BitCastFn(&repl_negate)}}); + ASSERT_TRUE(ov); + EXPECT_EQ(call_slot(inst, kBeta, 5), -5); // overlaid + EXPECT_EQ(call_slot(inst, kAlpha, 5), 15); // preserved: alpha -> x+10 + ov.reset(); // restore the vptr before freeing the object + Cpp::Destruct(inst, B); +} + +TEST(VTableOverlay, PreservesPrefixAndUnrelatedSlots) { + auto B = DeclareBase(); + void* inst = Cpp::Construct(B).data; + ASSERT_NE(inst, nullptr); + void** aot = *reinterpret_cast(inst); + // Prefix slot(s) immediately before the address point: Itanium has + // offset-to-top at [-2] and type_info at [-1]; MSVC has just the + // complete-object-locator at [-1]. + void* prefix_m1 = aot[-1]; +#ifndef _WIN32 + void* prefix_m2 = aot[-2]; +#endif + void* alpha_slot = aot[kAlpha]; + auto ov = Cpp::MakeUniqueVTableOverlay( + inst, B, {{Method(B, "beta"), TestUtils::BitCastFn(&repl_negate)}}); + ASSERT_TRUE(ov); + void** now = *reinterpret_cast(inst); + EXPECT_EQ(now[-1], prefix_m1); +#ifndef _WIN32 + EXPECT_EQ(now[-2], prefix_m2); +#endif + EXPECT_EQ(now[kAlpha], alpha_slot); // unrelated slot copied verbatim + ov.reset(); + Cpp::Destruct(inst, B); +} + +TEST(VTableOverlay, RestoresOnDestroy) { + auto B = DeclareBase(); + void* inst = Cpp::Construct(B).data; + ASSERT_NE(inst, nullptr); + void* aot = *reinterpret_cast(inst); + { + auto ov = Cpp::MakeUniqueVTableOverlay( + inst, B, {{Method(B, "beta"), TestUtils::BitCastFn(&repl_negate)}}); + ASSERT_TRUE(ov); + EXPECT_NE(*reinterpret_cast(inst), aot); + } + EXPECT_EQ(*reinterpret_cast(inst), aot); + EXPECT_EQ(call_slot(inst, kBeta, 5), 25); // original beta restored: x+20 + Cpp::Destruct(inst, B); // overlay already released by the inner scope +} + +TEST(VTableOverlay, ReplacesMultipleSlots) { + auto B = DeclareBase(); + void* inst = Cpp::Construct(B).data; + ASSERT_NE(inst, nullptr); + auto ov = Cpp::MakeUniqueVTableOverlay( + inst, B, + {{Method(B, "alpha"), TestUtils::BitCastFn(&repl_negate)}, + {Method(B, "beta"), TestUtils::BitCastFn(&repl_double)}}); + ASSERT_TRUE(ov); + EXPECT_EQ(call_slot(inst, kAlpha, 5), -5); + EXPECT_EQ(call_slot(inst, kBeta, 5), 10); + ov.reset(); + Cpp::Destruct(inst, B); +} + +TEST(VTableOverlay, RejectsInvalidInput) { + auto B = DeclareBase(); + void* inst = Cpp::Construct(B).data; + ASSERT_NE(inst, nullptr); + Cpp::ConstFuncRef beta = Method(B, "beta"); + Cpp::ConstFuncRef none = nullptr; + void* fn = TestUtils::BitCastFn(&repl_negate); + + EXPECT_EQ(Cpp::MakeVTableOverlay(nullptr, B, &beta, &fn, 1), nullptr); // inst + EXPECT_EQ(Cpp::MakeVTableOverlay(inst, nullptr, &beta, &fn, 1), + nullptr); // base + EXPECT_EQ(Cpp::MakeVTableOverlay(inst, B, &none, &fn, 1), nullptr); // method + Cpp::Destruct(inst, B); +} + +// Sibling instance of the same type is unaffected when another instance is +// overlaid -- proves per-instance scope of the vptr swap (not per-class). +TEST(VTableOverlay, OverlayIsPerInstance) { + auto B = DeclareBase(); + void* a = Cpp::Construct(B).data; + void* b = Cpp::Construct(B).data; + ASSERT_NE(a, nullptr); + ASSERT_NE(b, nullptr); + void* b_vptr_before = *reinterpret_cast(b); + + auto ov = Cpp::MakeUniqueVTableOverlay( + a, B, {{Method(B, "beta"), TestUtils::BitCastFn(&repl_negate)}}); + ASSERT_TRUE(ov); + + EXPECT_NE(*reinterpret_cast(a), b_vptr_before); // a swapped + EXPECT_EQ(*reinterpret_cast(b), b_vptr_before); // b untouched + EXPECT_EQ(call_slot(a, kBeta, 5), -5); // overlay on a + EXPECT_EQ(call_slot(b, kBeta, 5), 25); // original on b + + ov.reset(); + Cpp::Destruct(a, B); + Cpp::Destruct(b, B); +} + +// Thunks receive the unmodified `this` pointer of the overlaid instance, so +// they can read the object's data members directly. Layout assumed by the +// test: [ vptr ][ int value ] (true on every ABI we build for). +TEST(VTableOverlay, ThunkReadsThisAndDataMember) { + Cpp::CreateInterpreter({"-include", "new"}); + Cpp::Declare("struct OverlayWithData {" + " int value;" + " OverlayWithData() : value(100) {}" + " virtual ~OverlayWithData() {}" + " virtual int frob(int x) { return x; }" + "};"); + auto T = Cpp::GetNamed("OverlayWithData"); + ASSERT_NE(T, nullptr); + void* inst = Cpp::Construct(T).data; + ASSERT_NE(inst, nullptr); + + auto ov = Cpp::MakeUniqueVTableOverlay( + inst, T, {{Method(T, "frob"), TestUtils::BitCastFn(&repl_read_value)}}); + ASSERT_TRUE(ov); + + // First user virtual lives at kAlpha (Itanium D1/D0 prefix; MSVC single + // deleting-dtor) -- the same layout the existing tests use. + EXPECT_EQ(call_slot(inst, kAlpha, 5), 105); // value(=100) + x(=5) + ov.reset(); + Cpp::Destruct(inst, T); +} + +// Derived class with an override has the same single-vtable layout as the +// base; overlay still finds and replaces the (overridden) slot. +TEST(VTableOverlay, DerivedClassWithOverride) { + Cpp::CreateInterpreter({"-include", "new"}); + Cpp::Declare("struct DvBase {" + " virtual ~DvBase() {}" + " virtual int frob(int x) { return x + 1; }" + "};" + "struct DvDerived : DvBase {" + " int frob(int x) override { return x + 2; }" + "};"); + auto D = Cpp::GetNamed("DvDerived"); + ASSERT_NE(D, nullptr); + void* inst = Cpp::Construct(D).data; + ASSERT_NE(inst, nullptr); + + // Sanity-check the derived's original slot dispatches to DvDerived::frob. + EXPECT_EQ(call_slot(inst, kAlpha, 5), 7); + + auto ov = Cpp::MakeUniqueVTableOverlay( + inst, D, {{Method(D, "frob"), TestUtils::BitCastFn(&repl_negate)}}); + ASSERT_TRUE(ov); + EXPECT_EQ(call_slot(inst, kAlpha, 5), -5); + ov.reset(); + Cpp::Destruct(inst, D); +} + +// Three-level single-inheritance chain (A <- B <- C). Slot layout is still +// flat (one vptr), so overlay on C through C's scope replaces the slot. +TEST(VTableOverlay, MultiLevelInheritance) { + Cpp::CreateInterpreter({"-include", "new"}); + Cpp::Declare("struct MlA { virtual ~MlA() {} virtual int frob(int x) { return x + 1; } };" + "struct MlB : MlA { int frob(int x) override { return x + 2; } };" + "struct MlC : MlB { int frob(int x) override { return x + 3; } };"); + auto C = Cpp::GetNamed("MlC"); + ASSERT_NE(C, nullptr); + void* inst = Cpp::Construct(C).data; + ASSERT_NE(inst, nullptr); + + EXPECT_EQ(call_slot(inst, kAlpha, 5), 8); // MlC::frob: x+3 + + auto ov = Cpp::MakeUniqueVTableOverlay( + inst, C, {{Method(C, "frob"), TestUtils::BitCastFn(&repl_double)}}); + ASSERT_TRUE(ov); + EXPECT_EQ(call_slot(inst, kAlpha, 5), 10); // overlay: x*2 + ov.reset(); + Cpp::Destruct(inst, C); +} + +// Multiple inheritance with two polymorphic direct bases produces two +// vptrs (one per non-empty base subobject); the primary-vptr-only overlay +// cannot retarget the secondary-base dispatch path. MakeVTableOverlay +// refuses such a layout outright so the caller never gets back a handle +// that would mis-dispatch through the untouched secondary vptr. +TEST(VTableOverlay, RejectsMultipleInheritance) { + Cpp::CreateInterpreter({"-include", "new"}); + Cpp::Declare("struct MiA { virtual ~MiA() {} virtual int af(int x) { return x + 10; } };" + "struct MiB { virtual ~MiB() {} virtual int bf(int x) { return x + 20; } };" + "struct MiC : MiA, MiB {" + " int af(int x) override { return x + 11; }" + " int bf(int x) override { return x + 22; }" + "};"); + auto C = Cpp::GetNamed("MiC"); + ASSERT_NE(C, nullptr); + void* inst = Cpp::Construct(C).data; + ASSERT_NE(inst, nullptr); + + auto ov = Cpp::MakeUniqueVTableOverlay( + inst, C, {{Method(C, "af"), TestUtils::BitCastFn(&repl_negate)}}); + EXPECT_FALSE(ov); // refuses the layout + + Cpp::Destruct(inst, C); +} + +// Non-polymorphic class has no vtable to overlay; rejected at the +// RD->isPolymorphic() gate inside MakeVTableOverlay. +TEST(VTableOverlay, RejectsNonPolymorphicBase) { + Cpp::CreateInterpreter({"-include", "new"}); + Cpp::Declare("struct NonPoly { int x; };" + "struct PolyMethodHolder {" + " virtual int dummy(int x) { return x; }" + "};"); + auto NP = Cpp::GetNamed("NonPoly"); + auto PH = Cpp::GetNamed("PolyMethodHolder"); + ASSERT_NE(NP, nullptr); + ASSERT_NE(PH, nullptr); + void* inst = Cpp::Construct(NP).data; + ASSERT_NE(inst, nullptr); + Cpp::ConstFuncRef dummy = Method(PH, "dummy"); + void* fn = TestUtils::BitCastFn(&repl_negate); + EXPECT_EQ(Cpp::MakeVTableOverlay(inst, NP, &dummy, &fn, 1), nullptr); + Cpp::Destruct(inst, NP); +} + +// A virtual method from an unrelated, larger class has a slot index that +// exceeds the target base's vtable size; applyVTableOverlay's per-slot +// bounds check rejects rather than writing past the overlay block. +TEST(VTableOverlay, RejectsOutOfRangeMethodSlot) { + Cpp::CreateInterpreter({"-include", "new"}); + Cpp::Declare("struct SmallBase {" + " virtual ~SmallBase() {}" + " virtual int sf(int x) { return x; }" + "};" + "struct LargeUnrelated {" + " virtual ~LargeUnrelated() {}" + " virtual int v1(int) { return 0; }" + " virtual int v2(int) { return 0; }" + " virtual int v3(int) { return 0; }" + " virtual int v4(int) { return 0; }" + " virtual int v5(int) { return 0; }" + " virtual int v6(int) { return 0; }" + " virtual int v7(int) { return 0; }" + " virtual int v8(int) { return 0; }" + " virtual int v9(int) { return 0; }" + " virtual int v10(int) { return 0; }" + "};"); + auto Small = Cpp::GetNamed("SmallBase"); + auto Large = Cpp::GetNamed("LargeUnrelated"); + ASSERT_NE(Small, nullptr); + ASSERT_NE(Large, nullptr); + void* inst = Cpp::Construct(Small).data; + ASSERT_NE(inst, nullptr); + Cpp::ConstFuncRef v10 = Method(Large, "v10"); + void* fn = TestUtils::BitCastFn(&repl_negate); + EXPECT_EQ(Cpp::MakeVTableOverlay(inst, Small, &v10, &fn, 1), nullptr); + Cpp::Destruct(inst, Small); +} + +// Overlay across a A <- B hierarchy using replacements that read the +// object's data members through typed static_cast on `self`. Objects are +// stack-allocated by the test TU (matching the interpreter-side layout the +// reflection API computes the slot indices for) and initialised with +// non-zero member values; the replacements pull those values back out and +// fold them into the return, exercising the live-`this` path end to end. +TEST(VTableOverlay, OverlayThroughHierarchyAccessesDataMembers) { + Cpp::CreateInterpreter({}); + Cpp::Declare("struct A {" + " int m_x;" + " A(int x) : m_x(x) {}" + " virtual ~A() {}" + " virtual int method() { return m_x; }" + "};" + "struct B : A {" + " double m_d;" + " B(int x, double d) : A(x), m_d(d) {}" + " int method() override { return (int)m_d; }" + "};"); + auto A_scope = Cpp::GetNamed("A"); + auto B_scope = Cpp::GetNamed("B"); + ASSERT_NE(A_scope, nullptr); + ASSERT_NE(B_scope, nullptr); + + A a(5); + B b(7, 3.5); + + // Baseline: the host-compiler-built objects dispatch to their C++ bodies. + EXPECT_EQ(call_slot_no_arg(&a, kAlpha), 5); // A::method returns m_x + EXPECT_EQ(call_slot_no_arg(&b, kAlpha), 3); // B::method returns (int)m_d + + auto ov_a = Cpp::MakeUniqueVTableOverlay( + &a, A_scope, + {{Method(A_scope, "method"), TestUtils::BitCastFn(&foo)}}); + ASSERT_TRUE(ov_a); + auto ov_b = Cpp::MakeUniqueVTableOverlay( + &b, B_scope, + {{Method(B_scope, "method"), TestUtils::BitCastFn(&bar)}}); + ASSERT_TRUE(ov_b); + + EXPECT_EQ(call_slot_no_arg(&a, kAlpha), 15); // foo: A::m_x(5) + 10 + EXPECT_EQ(call_slot_no_arg(&b, kAlpha), 10); // bar: (int)(7 + 3.5) + + ov_a.reset(); + ov_b.reset(); + // No Destruct: stack-allocated objects, C++ dtors fire on scope exit + // (vptrs restored above so virtual dispatch lands on the real dtor). +} + +// Virtual inheritance has a longer pre-address-point prefix (vbase-offset +// entries) and a vtable-in-vbase that carries `_ZTv0_n*` virtual thunks for +// dispatch through the virtual-base pointer -- neither is covered by the +// primary-vptr overlay. MakeVTableOverlay refuses, mirroring the multi- +// inheritance case. Reviewer ref: stackoverflow.com/a/39182009. +TEST(VTableOverlay, RejectsVirtualInheritance) { + Cpp::CreateInterpreter({"-include", "new"}); + Cpp::Declare("struct ViBase {" + " virtual ~ViBase() {}" + " virtual int frob(int x) { return x + 1; }" + "};" + "struct ViDerived : virtual ViBase {" + " int frob(int x) override { return x + 2; }" + "};"); + auto D = Cpp::GetNamed("ViDerived"); + ASSERT_NE(D, nullptr); + void* inst = Cpp::Construct(D).data; + ASSERT_NE(inst, nullptr); + + auto ov = Cpp::MakeUniqueVTableOverlay( + inst, D, {{Method(D, "frob"), TestUtils::BitCastFn(&repl_negate)}}); + EXPECT_FALSE(ov); // refuses the layout + + Cpp::Destruct(inst, D); +} + +// on_destroy fires once on the deleting-dtor path (before the original +// destructor); receives `inst` and `cleanup_data` verbatim. +TEST(VTableOverlay, DestructorHookFires) { + auto B = DeclareBase(); + void* inst = Cpp::Construct(B).data; + ASSERT_NE(inst, nullptr); + + struct State { int fires = 0; void* last_inst = nullptr; }; + State state; + auto* ov = Cpp::MakeVTableOverlay( + inst, B, /*methods=*/nullptr, /*overlay_fns=*/nullptr, + /*n_overlays=*/0, /*n_extra_prefix_slots=*/0, + /*on_destroy=*/ + [](void* i, void* data) { + auto* s = static_cast(data); + s->fires += 1; + s->last_inst = i; + }, + /*cleanup_data=*/&state); + ASSERT_NE(ov, nullptr); + EXPECT_EQ(state.fires, 0); // not fired yet + + Cpp::Destruct(inst, B); // runs the wrapped deleting dtor + + EXPECT_EQ(state.fires, 1); + EXPECT_EQ(state.last_inst, inst); + + // Overlay handle outlives the instance: DestroyVTableOverlay must + // skip the vptr restore (instance freed) but still free the block. + Cpp::DestroyVTableOverlay(ov); +} + +// Two overlays installed via different interpreters: each carries its +// own hook state on its own block, so destruction of one does not fire +// the other's callback. Routing is per-instance (via the hidden +// self-pointer slot in each block), not per-interpreter. +TEST(VTableOverlay, DestructorHookIsPerInstance) { + Cpp::InterpRef I1 = Cpp::CreateInterpreter({"-include", "new"}); + ASSERT_NE(I1, nullptr); + Cpp::Declare("struct DhBase1 { virtual ~DhBase1() {} " + "virtual int frob(int x) { return x + 1; } };"); + auto B1 = Cpp::GetNamed("DhBase1"); + ASSERT_NE(B1, nullptr); + void* inst1 = Cpp::Construct(B1).data; + ASSERT_NE(inst1, nullptr); + int counter1 = 0; + auto* ov1 = Cpp::MakeVTableOverlay( + inst1, B1, nullptr, nullptr, 0, 0, + [](void* /*i*/, void* data) { *static_cast(data) += 1; }, + &counter1); + ASSERT_NE(ov1, nullptr); + + Cpp::InterpRef I2 = Cpp::CreateInterpreter({"-include", "new"}); + ASSERT_NE(I2, nullptr); + Cpp::Declare("struct DhBase2 { virtual ~DhBase2() {} " + "virtual int frob(int x) { return x + 2; } };"); + auto B2 = Cpp::GetNamed("DhBase2"); + ASSERT_NE(B2, nullptr); + void* inst2 = Cpp::Construct(B2).data; + ASSERT_NE(inst2, nullptr); + int counter2 = 0; + auto* ov2 = Cpp::MakeVTableOverlay( + inst2, B2, nullptr, nullptr, 0, 0, + [](void* /*i*/, void* data) { *static_cast(data) += 1; }, + &counter2); + ASSERT_NE(ov2, nullptr); + + // Re-activate I1 so Cpp::Destruct's reflection lookup runs against + // the scope's owning interpreter; only counter1 must fire. + Cpp::ActivateInterpreter(I1); + Cpp::Destruct(inst1, B1); + EXPECT_EQ(counter1, 1); + EXPECT_EQ(counter2, 0); + + Cpp::ActivateInterpreter(I2); + Cpp::Destruct(inst2, B2); + EXPECT_EQ(counter1, 1); + EXPECT_EQ(counter2, 1); + + Cpp::DestroyVTableOverlay(ov1); + Cpp::DestroyVTableOverlay(ov2); +} + +// on_destroy = nullptr is the "opt-in, zero overhead" contract: no +// wrapper is installed, so the deleting-dtor slot in the overlay block +// is a verbatim copy of the original. Pin that directly rather than +// inferring it from other tests passing. +TEST(VTableOverlay, DestructorHookOptInZero) { + auto B = DeclareBase(); + void* inst = Cpp::Construct(B).data; + ASSERT_NE(inst, nullptr); + void** orig_vptr = *reinterpret_cast(inst); +#ifdef _WIN32 + constexpr int kDDtor = 0; +#else + constexpr int kDDtor = 1; +#endif + void* orig_d = orig_vptr[kDDtor]; + + auto* ov = Cpp::MakeVTableOverlay( + inst, B, nullptr, nullptr, 0, 0, + /*on_destroy=*/nullptr, /*cleanup_data=*/nullptr); + ASSERT_NE(ov, nullptr); + + void** new_vptr = *reinterpret_cast(inst); + EXPECT_EQ(new_vptr[kDDtor], orig_d); + + Cpp::DestroyVTableOverlay(ov); + Cpp::Destruct(inst, B); +} + +// Caller-driven teardown before the C++ destruction restores the +// original vptr in ~VTableOverlay, so the wrapper is no longer +// installed and Cpp::Destruct calls the unhooked deleting dtor; the +// callback does not fire. +TEST(VTableOverlay, DestructorHookSkippedOnCallerTeardown) { + auto B = DeclareBase(); + void* inst = Cpp::Construct(B).data; + ASSERT_NE(inst, nullptr); + + int fires = 0; + auto* ov = Cpp::MakeVTableOverlay( + inst, B, nullptr, nullptr, 0, 0, + [](void* /*i*/, void* data) { *static_cast(data) += 1; }, &fires); + ASSERT_NE(ov, nullptr); + + Cpp::DestroyVTableOverlay(ov); + Cpp::Destruct(inst, B); + EXPECT_EQ(fires, 0); +} + +// Phase 1 extra-prefix slots: a binding stashes per-instance data in slots +// immediately before the ABI prefix; thunks read it via the inline +// VTableOverlayExtraSlot helper -- a single fixed-offset load. +TEST(VTableOverlay, SetsExtraPrefixSlots) { + auto B = DeclareBase(); + void* inst = Cpp::Construct(B).data; + ASSERT_NE(inst, nullptr); + auto ov = Cpp::MakeUniqueVTableOverlay( + inst, B, {{Method(B, "beta"), TestUtils::BitCastFn(&repl_negate)}}, + /*n_extra_prefix_slots=*/2); + ASSERT_TRUE(ov); + void*& slot0 = Cpp::VTableOverlayExtraSlot(inst, 0); + void*& slot1 = Cpp::VTableOverlayExtraSlot(inst, 1); + EXPECT_EQ(slot0, nullptr); // nullptr-initialized + EXPECT_EQ(slot1, nullptr); + slot0 = reinterpret_cast(uintptr_t{0xC0FFEE}); + slot1 = reinterpret_cast(uintptr_t{0xDEADBEEF}); + EXPECT_EQ(slot0, reinterpret_cast(uintptr_t{0xC0FFEE})); + EXPECT_EQ(slot1, reinterpret_cast(uintptr_t{0xDEADBEEF})); + // The overlaid slot still dispatches normally; extra-slot data is opaque. + EXPECT_EQ(call_slot(inst, kBeta, 5), -5); + ov.reset(); + Cpp::Destruct(inst, B); +} diff --git a/interpreter/CppInterOp/unittests/CppInterOp/VariableReflectionTest.cpp b/interpreter/CppInterOp/unittests/CppInterOp/VariableReflectionTest.cpp index a9dc9be898f13..7648cae52cedf 100644 --- a/interpreter/CppInterOp/unittests/CppInterOp/VariableReflectionTest.cpp +++ b/interpreter/CppInterOp/unittests/CppInterOp/VariableReflectionTest.cpp @@ -39,9 +39,9 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, VariableReflection_GetDatamembers) { }; )"; - std::vector datamembers; - std::vector datamembers1; - std::vector datamembers2; + std::vector datamembers; + std::vector datamembers1; + std::vector datamembers2; GetAllTopLevelDecls(code, Decls); Cpp::GetDatamembers(Decls[0], datamembers); Cpp::GetDatamembers(Decls[1], datamembers1); @@ -126,9 +126,9 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, VariableReflection_DatamembersWithAnonymousStru #undef Stringify #undef CODE - std::vector datamembers_klass1; - std::vector datamembers_klass2; - std::vector datamembers_klass3; + std::vector datamembers_klass1; + std::vector datamembers_klass2; + std::vector datamembers_klass3; Cpp::GetDatamembers(Decls[0], datamembers_klass1); Cpp::GetDatamembers(Decls[2], datamembers_klass2); @@ -184,11 +184,10 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, VariableReflection_GetTypeAsString) { TestFixture::CreateInterpreter(); EXPECT_EQ(Cpp::Declare(code.c_str()), 0); - Cpp::TCppScope_t wrapper = - Cpp::GetScopeFromCompleteName("my_namespace::Wrapper"); + Cpp::DeclRef wrapper = Cpp::GetScopeFromCompleteName("my_namespace::Wrapper"); EXPECT_TRUE(wrapper); - std::vector datamembers; + std::vector datamembers; Cpp::GetDatamembers(wrapper, datamembers); EXPECT_EQ(datamembers.size(), 1); @@ -280,7 +279,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, VariableReflection_GetVariableOffset) { EXPECT_EQ(7, Decls.size()); - std::vector datamembers; + std::vector datamembers; Cpp::GetDatamembers(Decls[4], datamembers); EXPECT_TRUE((bool)Cpp::GetVariableOffset(Decls[0])); // a @@ -297,7 +296,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, VariableReflection_GetVariableOffset) { EXPECT_EQ(Cpp::GetVariableOffset(datamembers[3]), ((intptr_t) & (c.d)) - ((intptr_t) & (c.a))); - auto* VD_C_s_a = Cpp::GetNamed("s_a", Decls[4]); // C::s_a + auto VD_C_s_a = Cpp::GetNamed("s_a", Decls[4]); // C::s_a EXPECT_TRUE((bool)Cpp::GetVariableOffset(VD_C_s_a)); struct K { @@ -306,7 +305,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, VariableReflection_GetVariableOffset) { int z; }; Cpp::Declare("struct K;"); - Cpp::TCppScope_t k = Cpp::GetNamed("K"); + Cpp::DeclRef k = Cpp::GetNamed("K"); EXPECT_TRUE(k); Cpp::Declare("struct K { int x; int y; int z; };"); @@ -325,17 +324,17 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, VariableReflection_GetVariableOffset) { template T constexpr ClassWithStatic::ref_value = 42; )"); - Cpp::TCppScope_t klass = Cpp::GetNamed("ClassWithStatic"); + Cpp::DeclRef klass = Cpp::GetNamed("ClassWithStatic"); EXPECT_TRUE(klass); ASTContext& C = Interp->getCI()->getASTContext(); std::vector template_args = { {C.IntTy.getAsOpaquePtr()}}; - Cpp::TCppScope_t klass_instantiated = + Cpp::DeclRef klass_instantiated = Cpp::InstantiateTemplate(klass, template_args); EXPECT_TRUE(klass_instantiated); - Cpp::TCppScope_t var = Cpp::GetNamed("ref_value", klass_instantiated); + Cpp::DeclRef var = Cpp::GetNamed("ref_value", klass_instantiated); EXPECT_TRUE(var); EXPECT_TRUE(Cpp::GetVariableOffset(var)); @@ -394,19 +393,19 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, VariableReflection_VariableOffsetsWithInheritan #undef Stringify #undef CODE - Cpp::TCppScope_t myklass = Cpp::GetNamed("MyKlass"); + Cpp::DeclRef myklass = Cpp::GetNamed("MyKlass"); EXPECT_TRUE(myklass); size_t num_bases = Cpp::GetNumBases(myklass); EXPECT_EQ(num_bases, 2); - std::vector datamembers; + std::vector datamembers; Cpp::GetDatamembers(myklass, datamembers); for (size_t i = 0; i < num_bases; i++) { - Cpp::TCppScope_t base = Cpp::GetBaseClass(myklass, i); + Cpp::DeclRef base = Cpp::GetBaseClass(myklass, i); EXPECT_TRUE(base); for (size_t i = 0; i < Cpp::GetNumBases(base); i++) { - Cpp::TCppScope_t bbase = Cpp::GetBaseClass(base, i); + Cpp::DeclRef bbase = Cpp::GetBaseClass(base, i); EXPECT_TRUE(base); Cpp::GetDatamembers(bbase, datamembers); } @@ -584,10 +583,10 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, VariableReflection_StaticConstExprDatamember) { : public integral_constant {}; )"); - Cpp::TCppScope_t MyClass = Cpp::GetNamed("MyClass"); + Cpp::DeclRef MyClass = Cpp::GetNamed("MyClass"); EXPECT_TRUE(MyClass); - std::vector datamembers; + std::vector datamembers; Cpp::GetStaticDatamembers(MyClass, datamembers); EXPECT_EQ(datamembers.size(), 1); @@ -598,7 +597,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, VariableReflection_StaticConstExprDatamember) { std::vector template_args = { {C.IntTy.getAsOpaquePtr(), "5"}}; - Cpp::TCppFunction_t MyTemplatedClass = Cpp::InstantiateTemplate( + Cpp::DeclRef MyTemplatedClass = Cpp::InstantiateTemplate( Cpp::GetNamed("MyTemplatedClass"), template_args); EXPECT_TRUE(MyTemplatedClass); @@ -612,13 +611,13 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, VariableReflection_StaticConstExprDatamember) { std::vector ele_template_args = { {C.IntTy.getAsOpaquePtr()}, {C.FloatTy.getAsOpaquePtr()}}; - Cpp::TCppFunction_t Elements = + Cpp::DeclRef Elements = Cpp::InstantiateTemplate(Cpp::GetNamed("Elements"), ele_template_args); EXPECT_TRUE(Elements); EXPECT_EQ(1, Cpp::GetNumBases(Elements)); - Cpp::TCppScope_t IC = Cpp::GetBaseClass(Elements, 0); + Cpp::DeclRef IC = Cpp::GetBaseClass(Elements, 0); datamembers.clear(); Cpp::GetStaticDatamembers(IC, datamembers); @@ -640,15 +639,15 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, }; )"); - Cpp::TCppScope_t MyEnumClass = Cpp::GetNamed("MyEnumClass"); + Cpp::DeclRef MyEnumClass = Cpp::GetNamed("MyEnumClass"); EXPECT_TRUE(MyEnumClass); - std::vector datamembers; + std::vector datamembers; Cpp::GetEnumConstantDatamembers(MyEnumClass, datamembers); EXPECT_EQ(datamembers.size(), 9); EXPECT_TRUE(Cpp::IsEnumType(Cpp::GetVariableType(datamembers[0]))); - std::vector datamembers2; + std::vector datamembers2; Cpp::GetEnumConstantDatamembers(MyEnumClass, datamembers2, false); EXPECT_EQ(datamembers2.size(), 6); } diff --git a/interpreter/CppInterOp/utils/TableGen/CppInterOpEmitter.cpp b/interpreter/CppInterOp/utils/TableGen/CppInterOpEmitter.cpp index 338a28fbd0aef..5d455432dd00d 100644 --- a/interpreter/CppInterOp/utils/TableGen/CppInterOpEmitter.cpp +++ b/interpreter/CppInterOp/utils/TableGen/CppInterOpEmitter.cpp @@ -15,7 +15,7 @@ // // where: // DispatchName — name in the dispatch table (unique, even for overloads) -// CppName — name in CppImpl:: namespace (may be shared by overloads) +// CppName — name in Cpp:: namespace (may be shared by overloads) // ReturnType — C++ return type string // DeclArgs — "(Type1 name1, Type2 name2, ...)" with defaults // CallArgs — "(name1, name2, ...)" for forwarding @@ -95,13 +95,13 @@ StringMap collectCTypeMaps(const RecordKeeper& Records) { StringMap Map; for (const auto* Rec : Records.getAllDerivedDefinitions("CTypeMap")) { CTypeMapEntry E; - StringRef CppType = Rec->getValueAsString("CppType"); + StringRef TypeRef = Rec->getValueAsString("TypeRef"); E.CType = Rec->getValueAsString("CType").str(); E.ArgConversion = Rec->getValueAsString("ArgConversion").str(); E.RetConversion = Rec->getValueAsString("RetConversion").str(); E.CollectionKind = Rec->getValueAsString("CollectionKind").str(); E.ElementCType = Rec->getValueAsString("ElementCType").str(); - Map[CppType] = std::move(E); + Map[TypeRef] = std::move(E); } return Map; } @@ -282,25 +282,12 @@ static void emitDoxygen(const APIRecord& R, raw_ostream& OS) { } /// Determine the C++ container type for a tmp variable from the CTypeMap -/// CppType string. Strips trailing '&' from out-param types. -/// E.g. "std::vector&" → "std::vector". -static std::string getCppTmpType(StringRef CppType, StringRef ElemCType) { - // Strip trailing '&' for out-params. - StringRef Base = CppType.rtrim('&').rtrim(); - // Extract the container template: "std::vector" or "std::set". - size_t Open = Base.find('<'); - if (Open == StringRef::npos) - return Base.str(); - StringRef Container = Base.substr(0, Open); - - // Map C element type back to C++ element type for the container. - std::string CppElem; - if (ElemCType == "char*") - CppElem = "std::string"; - else - CppElem = ElemCType.str(); - - return (Container + "<" + CppElem + ">").str(); +/// TypeRef string. Strips trailing '&' from out-param types. +/// E.g. "std::vector&" → "std::vector". +static std::string getCppTmpType(StringRef TypeRef, StringRef /*ElemCType*/) { + // Strip trailing '&' for out-params; the resulting type is used directly + // as the C++ temporary. + return TypeRef.rtrim('&').rtrim().str(); } /// Emit code to convert a C++ collection (vector or set) into a @@ -425,13 +412,6 @@ void EmitCXCppInterOpDecl(const RecordKeeper& Records, raw_ostream& OS) { OS << "// Auto-generated by cppinterop-tblgen. Do not edit.\n"; OS << "// C API declarations that forward to the Cpp:: C++ API.\n"; OS << "// Prefix: " << CAPIPrefix << "\n\n"; - // In C++ mode, the C-compatible structs live inside namespace CppImpl. - // Pull them into the enclosing scope so extern "C" signatures compile. - OS << "#ifdef __cplusplus\n"; - OS << "using Cpp::CppInterOpArray;\n"; - OS << "using Cpp::CppInterOpStringArray;\n"; - OS << "using Cpp::TemplateArgInfo;\n"; - OS << "#endif\n\n"; auto TypeMap = collectCTypeMaps(Records); std::string WarningStr;