Skip to content

docs: dynamically loaded modules must export module_register() with extern "C" — requirement is undocumented and the failure mode points elsewhere #90

Description

@Jordanplus

Summary

The module factory loads out-of-tree components from shared libraries by looking up the unmangled symbol module_register:

  • construct_module() falls back to the dylib loader when the module type is unknown:
    sc_core::sc_module* construct_module(std::string moduletype, sc_core::sc_module_name name, cci::cci_value_list args)
    {
    gs::cci_constructor_vl m_fac;
    cci::cci_value v = cci::cci_value::from_json("\"" + moduletype + "\"");
    if (!v.try_get<gs::cci_constructor_vl>(m_fac)) {
    register_module_from_dylib(name);
    }
    if (v.try_get<gs::cci_constructor_vl>(m_fac)) {
    /**
    * (m_fac)(name, args) will return raw pointer of unmanaged dynamically allocated moduletype:
    * new moduletype(name, args)
    */
    sc_core::sc_module* mod = (m_fac)(name, args);
    if (mod) {
    m_constructedModules.emplace_back(mod);
    return mod;
    } else {
    SC_REPORT_ERROR("ModuleFactory", ("Null module type: " + moduletype).c_str());
    }
    } else {
    SC_REPORT_ERROR("ModuleFactory", ("Can't find module type: " + moduletype).c_str());
  • register_module_from_dylib() resolves the symbol by name:
    void register_module_from_dylib(sc_core::sc_module_name name)
    {
    std::string libname;
    std::vector<std::string> lib_types = { ".moduletype", ".dylib_path" };
    for (const auto& type : lib_types) {
    const std::string base_name = std::string(sc_module::name()) + "." + std::string(name) + type;
    if (m_broker.has_preset_value(base_name)) {
    libname = std::string(m_broker.get_preset_cci_value(base_name).get_string()) + "." +
    std::string(m_library_loader.get_lib_ext());
    }
    }
    qemu::LibraryLoaderIface::LibraryIfacePtr libraryHandle = m_library_loader.load_library(libname);
    if (!libraryHandle) {
    SCP_FATAL(())("Failed to load library {}: {}", libname, m_library_loader.get_last_error());
    }
    m_dls.insert(libraryHandle);
    void (*module_register)() = nullptr;
    if (libraryHandle->symbol_exists("module_register")) {
    module_register = reinterpret_cast<void (*)()>(libraryHandle->get_symbol("module_register"));
    module_register();
    } else {
    SCP_WARN(()) << "module_register not found in library " << libname;
    }
    }
    (symbol_exists("module_register") at L951)

This only works if the component declares the hook extern "C" — as every in-tree component does, e.g. keep_alive.h#L34. But nothing in the documentation states this requirement: the docs show how to use moduletype + dylib_path in Lua (e.g. docs/backends.md#L111-L127) but never mention module_register at all, let alone its required linkage (checked: no match for module_register or extern "C" anywhere under docs/ or in README.md on current main).

Failure mode when the requirement is missed

A plain C++ void module_register() { ... } compiles and links fine, but exports a mangled symbol (e.g. __Z15module_registerv). At runtime:

  1. The dylib loads successfully.
  2. symbol_exists("module_register") fails, producing only SCP_WARN(...) "module_register not found in library ..." (module_factory_container.h#L955), which is easy to miss at default log levels.
  3. Elaboration then aborts with SC_REPORT_ERROR: "Can't find module type: <X>" (module_factory_container.h#L98) — an error that suggests a wrong moduletype string or a missing/unfound library, not C++ name mangling. We lost real time to exactly this on an out-of-tree component before diffing against in-tree headers revealed the extern "C" convention.

Reproduction

  1. Create an out-of-tree component with void module_register() { GSC_MODULE_REGISTER_C(mycomp, ...); }without extern "C" — and build it as a shared library.
  2. Reference it from a platform config: mycomp_0 = { moduletype = "mycomp", dylib_path = "<path/to/lib>" , ... }.
  3. Run the platform: the library loads, registration is skipped with only a warning, and elaboration fails with Can't find module type: mycomp.
  4. Add extern "C" to the declaration; everything works.

Suggestions (scoped)

  1. Document the contract wherever dylib_path is introduced (e.g. docs/configuration.md and/or docs/backends.md): a dynamically loadable component must export extern "C" void module_register(); which performs the GSC_MODULE_REGISTER* call, matching the pattern used by all in-tree components.
  2. Optionally improve the diagnostic: when module_register is not found after a successful dylib load, mention the likely cause in the message (e.g. "symbol not found — is it declared extern "C"?"), and/or escalate the SCP_WARN to an error since a library named via dylib_path that exposes no module_register can never satisfy the lookup.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions