#  Boost.OpenMethod Library – dynamic_loading test Jamfile
#
#  Copyright 2025 Jean-Louis Leroy
#
#  Distributed under the Boost Software License, Version 1.0.
#  See accompanying file LICENSE_1_0.txt or copy at
#  http://www.boost.org/LICENSE_1_0.txt

# Reproduce the CMake dynamic-loading test in Boost.Build.
#
# Build graph (all shared libraries):
#
#   lib_registry  ──▶  lib_method  ──▶  lib_overrider
#        │                                    (runtime-loaded)
#        └──────────────────────┐
#   test executable  ──linked──▶ lib_registry
#                    ──dlopen──▶ lib_method, lib_overrider

import testing ;
import os ;

local os-name = [ os.name ] ;
local visibility-env = [ os.environ B2_VISIBILITY ] ;
local windir = [ os.environ WINDIR ] ;

# -rdynamic: export exe symbols to dynamically loaded libs so that
# dlopen'd overrider can resolve symbols from the already-loaded method lib.
local RDYNAMIC =
    <target-os>linux:<linkflags>"-rdynamic"
    <target-os>freebsd:<linkflags>"-rdynamic"
    <target-os>android:<linkflags>"-rdynamic"
    <target-os>darwin,<toolset>gcc:<linkflags>"-dynamic"
    <target-os>darwin,<toolset>clang:<linkflags>"-rdynamic"
    <target-os>qnxnto,<toolset>gcc:<linkflags>"-rdynamic"
    <target-os>solaris:<linkflags>"-Bdynamic" ;

# Disable Clang's -fsanitize=vptr sub-check under UBSAN. This test creates
# polymorphic objects inside dlopen'd libraries and downcasts them in another
# module (core.hpp optimal_cast, reached through open-method dispatch). The vptr
# check validates an object's vptr against the expected type's RTTI, but on
# platforms that do not deduplicate a class's typeinfo/vtable across DSOs
# (notably macOS/Mach-O) it fires a false positive even though the object really
# is of that type. Gate on the UBSAN feature so nothing changes elsewhere; it is
# a no-op for GCC (which has no vptr sanitizer). ASAN is unaffected.
#
# The `function` sub-check fires for the same underlying reason once these tests
# build at hidden visibility: it validates a call through a function pointer
# against the callee's type, and reports the dispatch thunk as "call to function
# ... through pointer to incorrect function type" when the two modules' type_info
# objects for that identical type differ. Gate it on the Clang toolsets: it is a
# Clang-only sub-check, and GCC rejects an unknown -fno-sanitize= argument.
local NO_VPTR =
    <undefined-sanitizer>on:<cxxflags>-fno-sanitize=vptr
    <undefined-sanitizer>norecover:<cxxflags>-fno-sanitize=vptr
    <toolset>clang,<undefined-sanitizer>on:<cxxflags>-fno-sanitize=function
    <toolset>clang,<undefined-sanitizer>norecover:<cxxflags>-fno-sanitize=function
    <toolset>clang-darwin,<undefined-sanitizer>on:<cxxflags>-fno-sanitize=function
    <toolset>clang-darwin,<undefined-sanitizer>norecover:<cxxflags>-fno-sanitize=function ;

# Link the external Boost utility libraries (dll, test, filesystem, and their
# transitive deps like system) statically. Only *our* registry/method/overrider
# libraries need to be shared objects; the Boost dependencies are pulled in via
# the propagated <link>shared and would otherwise be built as shared libraries
# too. That breaks under sanitizers:
#   * Clang UBSAN: Clang links the static UBSAN runtime, so a shared library is
#     left with undefined __ubsan_*_abort symbols; Boost.Filesystem builds with
#     -Wl,--no-undefined, turning those into a hard link error. (GCC links
#     libubsan into the DSO, so it is unaffected.)
#   * macOS UBSAN: a shared Boost.Test with hidden visibility gives each DSO its
#     own copy of typeinfo for lazy_ostream, tripping -fsanitize=vptr at runtime.
# Linking these dependencies statically into the executable side-steps both.

# Build one set of {registry, method, overrider} shared libraries plus a test
# executable. Each variant compiles the very same sources but with a different
# default registry, selected by extra requirements (e.g. a <define>) applied to
# every target so all modules agree (ODR). Targets are suffixed and outputs go
# to a per-variant <location-prefix> so that main.cpp, which locates its sibling
# libraries by filename fragment at runtime, never picks up another variant's
# libraries.
#
# Here lib_registry always owns and exports the single shared registry-state
# symbol; method/overrider/exe import it. The CMake build additionally has
# "exe-owned" variants (the executable owns the state and the libraries link
# against it). Those are CMake-only: b2 does not expose an executable's import
# library to dependent DLLs, so the reverse linkage they need cannot be
# expressed on Windows. See test/dynamic_loading/CMakeLists.txt.
rule make-variant ( suffix : extra-req * )
{
    # lib_registry: owns and exports the registry's static policy state.
    # registry.cpp defines EXPORT_REGISTRY (since REGISTRY_IN_EXE is unset).
    lib boost_openmethod-dl_test_registry$(suffix)
        : registry.cpp
        : <link>shared
          $(NO_VPTR)
          $(extra-req)
          <library>/boost/dll//boost_dll/<warnings>off/<link>static
          <location-prefix>dynamic_loading$(suffix).test
        ;

    # lib_method: exports the speak() open-method; imports registry from
    # lib_registry.
    lib boost_openmethod-dl_test_method$(suffix)
        : method.cpp
        : <link>shared
          $(NO_VPTR)
          $(extra-req)
          <library>/boost/dll//boost_dll/<warnings>off/<link>static
          <library>boost_openmethod-dl_test_registry$(suffix)
          <location-prefix>dynamic_loading$(suffix).test
        ;

    # lib_overrider: adds the Dog overrider; dynamically loaded at runtime.
    # It imports the shared registry state (extern template, via registry.hpp),
    # so it must link lib_registry, which *defines* that symbol. lib_method only
    # imports the symbol too, so linking it does not satisfy the reference. On
    # ELF an undefined symbol in a shared object is resolved lazily at load time,
    # but Mach-O (macOS) and PE (Windows) require it to be satisfied by a
    # directly linked module at link time, so link lib_registry unconditionally.
    lib boost_openmethod-dl_test_overrider$(suffix)
        : overrider.cpp
        : <link>shared
          $(NO_VPTR)
          $(extra-req)
          <library>/boost/dll//boost_dll/<warnings>off/<link>static
          <library>boost_openmethod-dl_test_method$(suffix)
          <library>boost_openmethod-dl_test_registry$(suffix)
          <location-prefix>dynamic_loading$(suffix).test
        ;

    # Test executable:
    #   sources (linked) : main.cpp, lib_registry, unit_test_framework, boost_dll
    #   runtime-loaded   : lib_method, lib_overrider
    # All shared libraries are placed alongside the test executable via
    # <location-prefix>dynamic_loading$(suffix).test, so main.cpp can find them
    # by scanning dll::program_location().parent_path() on all platforms.
    run main.cpp
        boost_openmethod-dl_test_registry$(suffix)
        /boost/test//boost_unit_test_framework/<warnings>off/<link>static
        /boost/dll//boost_dll/<warnings>off/<link>static
        :
        :
        : <link>shared
          $(RDYNAMIC)
          $(NO_VPTR)
          $(extra-req)
          <library>/boost/filesystem//boost_filesystem/<warnings>off/<link>static
          <target-os>linux:<linkflags>"-ldl"
          <target-os>freebsd:<linkflags>"-ldl"
          <target-os>android:<linkflags>"-ldl"
          <target-os>windows:<library>boost_openmethod-dl_test_registry$(suffix)
          <target-os>windows:<library>boost_openmethod-dl_test_method$(suffix)
          <target-os>cygwin:<library>boost_openmethod-dl_test_method$(suffix)
          <dependency>boost_openmethod-dl_test_method$(suffix)
          <dependency>boost_openmethod-dl_test_overrider$(suffix)
        : dynamic_loading$(suffix)
        ;
}

# Two variants (dll-owned registry state) x {default, indirect} registry. The
# exe-owned variants are CMake-only (see the note above).
#
# The colons in the registry's qualified name are escaped (\:) because b2's
# scanner treats ':' as a special character and otherwise warns "Unescaped
# special character in argument"; the escaped form still yields the define value
# BOOST_OPENMETHOD_DEFAULT_REGISTRY=::boost::openmethod::indirect_registry.
make-variant _default : ;
make-variant _indirect
    : <define>BOOST_OPENMETHOD_DEFAULT_REGISTRY=\:\:boost\:\:openmethod\:\:indirect_registry ;
