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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions