C++ RTTI Across Shared Libraries: Test Type Identity at the Plugin Boundary

A plugin boundary that exchanges polymorphic C++ objects needs more than matching exported function names. The host and plugin must agree on class definitions, runtime type information and exception identity. Test dynamic_cast and typed exception matching across the actual DSO boundary before treating the interface as compatible.

This guide uses GCC 14.2, its libstdc++ ABI documentation and GNU Binutils 2.44. An executed x86-64 fixture loaded an owned plugin with RTLD_LOCAL, cast a returned object and caught a plugin-thrown type. Both sides used the same GCC 14.2.0 runtime environment.

Declare the C++ boundary explicitly

List every class that crosses the boundary directly or through a base pointer, reference or exception. Preserve the common header and its build definitions. A factory with extern “C” can simplify symbol lookup, but returning a Base pointer still exposes a C++ object contract.

Record virtual functions, destructor ownership, inheritance, data layout and exception specifications. Conditional compilation in a public header can produce different class definitions under identical names. A successful symbol lookup does not validate those definitions.

Keep the creator responsible for destruction through a documented API unless the allocation/runtime contract explicitly supports another arrangement. Destroy every plugin-owned object before unloading the code that implements its virtual functions and destructor.

A plugin creates a polymorphic object, the host performs a downcast and typed exception catch, and destruction precedes unloading; exported RTTI and runtime limits are explicit.
Original cross-DSO type and lifetime map with executed positive host results.

Audit RTTI and exception build settings

The GCC 14.2 C++ options reference warns that mixing -frtti and -fno-rtti can fail. Some casts do not require runtime type information, so merely finding a dynamic_cast expression in a test does not prove that the relevant RTTI path was exercised.

Choose a downcast from a polymorphic base pointer to the expected derived type. Inspect the plugin and host for the associated type-info objects, type names and virtual tables as applicable. Use both dynamic and ordinary symbol views so hidden/local definitions are not mistaken for missing source generation.

Exception handling has its own support requirements. GCC can generate exception-related type information even under settings that suppress other RTTI uses. Do not infer a complete dynamic_cast contract from a successful catch, or a complete exception contract from a successful cast.

Make boundary-type visibility intentional

With hidden default visibility, annotate the public C++ types as well as the factory functions. The GCC visibility documentation specifically requires default visibility for exception classes thrown between DSOs so their type-info nodes can be unified.

A function export list alone does not express the whole type boundary. Review generated RTTI symbols and any visibility inherited from enclosing namespaces or template arguments. Keep the annotations in the common interface header so both producer and consumer see the same policy.

Do not use pointer equality between two arbitrarily obtained type_info addresses as a universal compatibility test. The meaningful language-level operations are type comparison, the required cast and the intended handler match, under the selected ABI and runtime. Raw address observations can supplement a loader investigation but cannot replace that contract.

Read the executed positive fixture

The common header declared a polymorphic Base, a Derived containing an integer value of 17 and a DomainError containing an error code. All three boundary classes had explicit default visibility. The plugin itself was compiled with -fvisibility=hidden, leaving its intended factories explicitly public.

The host loaded the plugin with RTLD_NOW | RTLD_LOCAL and resolved create, destroy and raise_error. The factory returned a Base pointer to a Derived. dynamic_cast to Derived succeeded, the value was 17 and typeid comparison agreed with Derived.

The plugin then threw DomainError with code 9. The host caught that exact type and verified the code. It destroyed the object through the plugin’s destroy function, completed the catch scope and only then closed the plugin handle.

The plugin’s dynamic definitions included the Base, Derived and DomainError type-info symbols. Those artifact checks and runtime operations passed on the matching host toolchain. No invalid mixed-RTTI, incompatible vendor or alternate-runtime combination was executed.

Use negative gates without running undefined contracts

A build that suppresses required RTTI, changes a class layout or hides a required exception type should fail review before a product launch. A controlled compiler diagnostic can be useful, but a deliberately incompatible object layout is not a safe way to prove runtime robustness.

For a target qualification matrix, vary only supported configurations. Include a valid cast to the expected derived type and a valid pointer cast to an unrelated derived type whose correct result is null. Separately test the intended typed exception and an unrelated exception handled by the defined fallback path.

Those extra cases are proposed here. Keep them distinct from the executed positive fixture so a reader does not mistake a recommended matrix for completed evidence.

Review runtime and loader identity

The GCC libstdc++ ABI policy helps separate compiler ABI, library ABI and configuration requirements. Record compiler versions, runtime providers, dual-ABI choices where relevant and the exact plugin/host hashes. One matching SONAME does not establish all those properties.

Inspect supported loader scopes and any local-binding options. A flag introduced to fix an unrelated symbol collision can alter cross-DSO identity assumptions. Re-run the type fixture after visibility, -Bsymbolic, runtime or plugin-loading changes.

Optional plugins also need a lifetime policy for outstanding objects, callbacks, threads and exception objects. Passing a cast once does not prove that unloading is safe while another thread still holds a vtable pointer into the plugin.

Accept a tested type interface

Preserve the common header, generated-symbol audit, loading mode and behavioral results. Require construction, casting, exception matching and destruction to complete under the actual supported runtime combination. Add target resource and failure tests separately.

If maintaining that C++ compatibility contract is impractical, a narrower opaque-handle interface can keep class layout and exceptions inside one component. That is an interface redesign requiring explicit ownership and error translation. The present result qualifies only the matching host fixture, while providing a concrete checklist for the real plugin boundary.

Similar Posts