Skip to content

feat(plugins): native plugins that load only into the build they target - #1374

Merged
alandefreitas merged 14 commits into
cppalliance:developfrom
alandefreitas:feat/support_plugins
Nov 11, 2026
Merged

alandefreitas merged 14 commits into
cppalliance:developfrom
alandefreitas:feat/support_plugins

Conversation

@alandefreitas

@alandefreitas alandefreitas commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator

This builds on Gennaro's plugin work #1256. His pull request added native plugins, but it had problems that would have taken many review rounds, so I fixed them on top of his commits instead. His four commits come first, unchanged, and the ten after them are mine.

Plugins turned out to have a lot of moving parts because of how different operating systems load them into the current process. A plugin sees our C++ types directly by their layout and visibility rules between libraries, dependencies, and plugins that interact. To make things harder, GLIBC (and its equivalents in other OSs) are among these libraries, which means significant CI changes since plugins only work on a release built on an OS as old as the one the plugin is built on. Otherwise, we wouldn’t support plugins on Linux or macOS at all. On top of that, plugins only work when they match very specific build properties: the same commit/ABI, a compatible standard library, and the same exported symbols. In these cases, the PR implemented the Python manylinux and clang conventions for plugins. CMake targets also changed considerably because they have many new, very specific visibility rules depending on their use cases.

All of these moving parts need to be maintained, and we need checks to ensure they won’t break in the future on any platform. For instance, getting a single MRDOCS_DECL or MRDOCS_VISIBLE wrong would break these invariants. For this reason, this PR also includes lots of linters and checks to make sure this doesn’t break (the PR uses Mr.Docs itself for this visibility check, so we found another use case). All invariants need to hold on every platform, with both scripting engines, in static and shared builds, and on old and new Linux distributions. Each of those became a rule with a check that fails CI when it does.

Changes

A plugin now loads only into the build it was compiled against, and scripts and plugins share one pipeline and one reporter (competing global symbols don’t work well when you load plugins). The changes were significant enough that the documentation and contributor guide had to be updated and reorganized, and the new rules needed new linters and tests:

Features

The main feature is complete native plugins that load only into the build they target

  • The plugin expresses the exact build identity tag with standard-library, ABI version, and any other flags. A plugin built with an incompatible standard library also fails at its first compile. If it’s loaded anyway, it fails at runtime.
  • The plugin session objects carry references to the mrdocs session (globals no longer exist) and provide registerTransform and registerGenerator that can take an id and a callable. The scripts only need a proxy of the session object now so this code has been simplified.
  • The Mr.Docs package includes mrdocs_add_plugin among other functions. This function can be used like the analogous cmake functions to create libraries. It creates a mr.docs plugin and set all appropriate visibility tags or any other requirements for the platform.
  • This involved some fixes like the library registry owning the dlopen/LoadLibraryEx loader (never unloaded) and fixes to file reads for UTF-8 paths on Windows.
  • A few refactors in the library API: MRDOCS_SOURCE has to be corrected and was also renamed; the functions registerTransform and registerGenerator are part of the plugin context. MRDOCS_DYNAMIC_LINK marks code that reaches MrDocs through another module, while the way to distinguish the conditions for __declspec(dllexport) changed because, unlike Boost libraries (where we got the pattern from), Mr.Docs needs __declspec(dllexport) on the static build as well.
  • In the documentation, the PR includes examples with plugins alongside the existing script examples. The PR then explains how they should match the build, docs(plugins): native plugins, what must match, building for plugins. Finally, it includes sections dedicated to native extensions.
  • On CMake, the targets had to change significantly. Only the MrDocs public API (no more, no less) should be exported from the tool, and plugins should follow the same pattern (enforced by `mrdocs_add_plugin`) so that there are no conflicts when the plugin is loaded into the same process. Otherwise, the wrong code would be executed. Also in CMake, we implemented a system where the export table is decided entirely by the MRDOCS_DECL markers. This is hard to achieve, and many tools resort to globs or just accept the errors. In our case, we used Mr.Docs itself to ensure we never get this wrong with an extension for export markers. The PR also provides the abstraction for shared builds to be able to load plugins with the exact same recipes, and the whole logic is adjusted under the hood.
  • When creating the target, the build ID marks modified trees and comes with source archives. DOM and handlebars symbols stay visible in a hidden core. The core's targets are set up once, and plugins only link mrdocs::plugin-api, which is a target that includes the headers with exported symbols but not definitions, because these come only once from the running process. Each class carries exactly the export marker its members need, considering the difference between platforms where we need MRDOCS_VISIBLE for type information only on Unix and Windows doesn’t allow visibility with duplicate sources.

Build

  • The bootstrap script had to be improved because of some of these requirements. We include new just recipes and IDE run configs for every test suite. The script now coordinates with CMake via the file API to find the real program paths in run configs and just recipes. The IDE config refresh keeps the saved bootstrap arguments. We include just recipes for every test suite.

Fixes

  • We had to fix the include-dir probes to the compiler and sysroot of the compile command instead of just the compiler. We also had to fix a bug where an empty Lua table wasn’t accepted for an object field because it affected the new proxies.

Refactors

As part of the new requirements coming from linters and limitations of plugins, we had to refactor part of the code. We have a session object now that owns its reporter and other classes related to the mr.docs engine. We also joined the pipelines for script extensions and native extensions into the same pipeline and documented them as a single thing.

  • We created a single dir layout for script and native extensions. We implemented support for flat extensions and extensions with their own directories with the logic for scripts and plugins, where the extension is named by its stem.
  • The script context became just a described proxy of the C++ context, simplifying the code. So a script's ctx is built next to its caller now. In C++, the generators and transforms can also be defined by lambdas or classes and be wrapped like the plugins do.
  • A new session object owns the reporters, generators and extensions. This reporter is passed to whoever reports. We have one pipeline of transforms and generators per-run for scripts and plugins. A generator receives its options block like a transform, with the options changing slightly because the generator receives a constant corpus.

Some smaller refactors came from hardened guards the code asked for as we had to review this long diff.

  • The linters are now incremental (judge only what the branch changes) and emit SARIF, which integrates more easily with other tools, including agents, and allows us to filter checks by line to match it to the diff. . reformat.py honours --check and takes the files and lines to format
  • Public type names are all in PascalCase so that we can enforce this as a guard with a Mr.Docs extension we included as a lint. Type names are also checked and are camelCase. Member functions are also covered with the appropriate exceptions for traits, and classes that express concepts like containers.
  • .clang-format was updated again to describe the code as written a little closer.
  • We also defined and applied linting for capping README.md length in subdirectories, forcing section titles to name the concept they discuss, ensure unit tests mirror the header they test.

Documentation

In the documentation:

  • The contributor guide was reorganized around the new codebase so that humans and agents can quickly understand the directory layout and the workflow hierarchically. For each step of the layout and the workflow, we provide details of the layout, application pipeline, how to set up the development environment with bootstrap.py, coding standards and their corresponding linters, invariants of the application in subpages with a page-length guard, and explain suites: table names, each suite, and its run command.
  • The user guide has sections explaining all of the features above. Also, copyable snippets come from built examples and tests. The snippets extension resolves includes on demand (a bug we had to fix where there are too many files in the extension for extra snippets), UI improvements on the selected tab label keep the on-accent ink, we fixed a bug so that registry check follows the registry schema in the reference page, we created a check so that links always target explicit section anchors, and the breaking-changes example session now shows sessions instead of the old globals.

Continuous Integration

In CI, we enforce release floors for Linux and macOS, shared rows, and the release checks. This is what ensures that our releases are going to work on other versions of these operating systems. For the Linux release, we have a glibc floor and a bundled C++ runtime to ensure Mr.Docs uses the same one it was built with, and then plugins can be loaded smoothly (same Python manylinux strategy). For macOS 14, we set a minimum for the release, and its LLVM and the macOS release check fails when the deployment target is not set.

Examples and tests

Examples and test suites were reorganized.

  • The compiled plugin examples live under examples/extensions. The stats generator and brief-filler transform are separate plugins now. We now have one directory per example with every variant (JS/Lua/C++). We create a load-and-run example for the library page. Each example has its own tests and one addon root per language.
  • The plugin and CMake package suites are tested under tests/integration. In the integration/plugin-api tests, the cases are grouped into exports, refused, dependencies, and reload. The CMake consumer tests in tests/integration/cmake test Mr.Docs as executable, plugin, and library parts.
  • Library unit tests also mirror their headers now (this was only enforced for main tests before).

Breaking changes

  • Scripts report through ctx.report.warn, ctx.report.error and ctx.report.info instead of the global mrdocs.report.
  • A script can no longer report while it loads, only inside a transform or a generator, because ctx exists only during those calls.
  • A Session is made with Session::create(config), which takes the configuration by value and returns an Expected<Session>. The session owns the configuration, and session.config() returns it.
  • Corpus::build takes the reporter as report::Reporter& instead of a std::shared_ptr, and the reporter has to outlive the corpus.
  • session.log() returns a report::Reporter&, so session.log()->warn(...) becomes session.log().warn(...).

Testing

Integration tests cover refused plugins, symbol isolation, dependencies, reloading and a consumer built with another compiler, and CI runs them on every compiler, static and shared.

Documentation

The plugin pages explain what must match and how to build a plugin, and the contributor guide documents each rule and the check that guards it.

Closes #1256

Fixes #58

gennaroprota and others added 14 commits October 21, 2026 10:23
`MRDOCS_DECL` expanded to nothing in a static build, so it marked
nothing and its misuse went unnoticed: it was applied to some classes
and to some of their members, which MSVC rejects. `MRDOCS_TOOL` now
comes first, so a plugin can call the symbols the headers declare, while
a static consumer still sees plain declarations.

Marking the API also exposed declarations that had no marker at all,
such as the symbol kinds' `operator<=>` and several `Corpus` members,
which a plugin reaches from inline and template code. We mark them now.

The dom and handlebars libraries check build defines of their own in the
same place, `MRDOCS_DOM_BUILDING` and `MRDOCS_HANDLEBARS_BUILDING`,
which only their own targets set, so that their headers do not depend on
the tool's.

We also declare `assert_failed`, with `MRDOCS_DECL`, in every build
mode: a plugin built without `NDEBUG` calls it from the inline code of
the public headers, whatever mode MrDocs was built in.
MrDocs loads the shared libraries in the plugins subdirectory of each
addon root as it starts up, and lets each one install a generator. A
plugin resolves the MrDocs symbols it calls against the tool, so writing
an output format takes the MrDocs headers and nothing else: no LLVM, no
Clang, no separate library to ship.

The tool links mrdocs-core privately now, so that a plugin linking the
tool does not link the core as well.

Note that a statically linked MrDocs, such as the Linux release, cannot
load plugins. `MRDOCS_BUILD_PLUGIN_MODULES` is off by default there, and
in a shared build, and a MrDocs built without it reports a plugin it
finds instead of loading it.

Closes cppalliance#58.
A plugin could install a generator, so it could add an output format but
not change what any format sees. We now let a plugin install corpus
transforms too, closing the gap with extension scripts.

`loadPlugins` returns them in a `PluginRegistry`, which the tool applies
before the script transforms, as it applies an `ExtensionRegistry`.
…le command

MrDocs finds the default include directories by running the compiler of each compile command with `-v -E`. It ran that compiler without the command's `-isysroot` or `--sysroot`, so Xcode's toolchain `clang++` listed no SDK headers and `<cstdint>` was missing. It also treated a bare name such as `clang++` as a file path, found nothing, and fell back to guessing a compiler.

The probe now passes the command's sysroot and looks a bare compiler name up on PATH, the way the driver runs it. It falls back to `$CXX`, `$CC` and common names only when the command's compiler cannot be found.
The guards that keep the rules of this branch from breaking later, and the sweeps that make the tree pass them:

- lint, format and the release checks each live in their own directory under `utils/`, and every guard judges only what a branch changes
- guards write SARIF, so CI and the review tools read the same findings
- a layout check, a README length cap, section titles that name their concept, and unit tests that mirror the header they test
- `.clang-format` describes the code as written
- public type and function names in PascalCase, function names at column 0 after the qualifier line, and `Platform.hpp` first in the headers that need it, checked by the self-doc run
- copyright lines where this branch changed existing files
- plugin support and the plugin tests are separate CMake options
- `just` has a recipe for every test suite
- the IDE run configs take program paths from CMake's file API, and refreshing them keeps the saved bootstrap arguments
- each CMake helper is a file of its own, and a runtime DLL already next to its consumer is not linked again
- the `find_tool` tests ignore the tool variables of the environment
…oever reports

The global report functions become a `report::Reporter` that the session owns. The corpus, the extensions and the generators receive it by reference instead of reaching for global state, so two sessions in one process no longer share a log level or an error count.

Breaking for library users: `session.log()` returns `report::Reporter&`, `Corpus::build` takes the reporter by reference, and scripts report through `ctx.report` instead of the global `mrdocs.report`.
… and plugins

Scripts and native plugins now go through the same per-run pipeline:

- one `extensions/` layout for scripts and plugins, and an extension is named by its file stem
- the registries of transforms and generators belong to the context of the run
- a script's `ctx` is the described C++ context, built next to its caller, with one builder per context type
- a generator receives its options block the way a transform does
- the extension contexts and callable wrappers live in headers of their own
This finishes the plugin support Gennaro started. A plugin is a shared library that registers transforms and generators with `registerTransform` and `registerGenerator`, each taking an id and a callable.

A plugin sees MrDocs's C++ types by their layout, so it must match the exact build:

- the loader (our own `dlopen` and `LoadLibraryEx`, never unloaded) refuses a plugin whose build id differs. The build id is the commit hash, with `.modified` for an edited tree, and comes from `git-archival.txt` in a source archive.
- a plugin built against another standard library fails at its first compile, and the loader checks the tag again.
- the tool exports only the MrDocs API. Each public class carries the marker its members need: `MRDOCS_DECL` when a member is compiled into MrDocs, `MRDOCS_VISIBLE` for type information only, and the self-doc run checks every case.
- dom and handlebars symbols stay visible in a hidden core, and shared builds load plugins with the same recipe.

Plugins link `mrdocs::plugin-api`, and `mrdocs_add_plugin` ships with the CMake package. The integration tests cover the refusals, symbol isolation, dependencies, reloading and a consumer built with another compiler. The user docs explain what must match and how to build a plugin.
- one directory per example with every variant and its tests, and the compiled plugin examples under `examples/extensions`
- a load-and-run example for the library page
- the plugin and CMake package suites under `tests/integration`, the plugin cases grouped into exports, refused, dependencies and reload, and the consumer test split into executable, plugin and library parts
- golden migration fixtures named after the migration they show
- library unit tests mirror their headers, and the tests share their setup instead of copying it
…hecks

- the Linux release builds in ubuntu:20.04 for a glibc 2.31 floor and bundles the C++ runtime
- the macOS release and its LLVM target macOS 14, and the release check fails when the deployment target is not set
- shared rows for every compiler family
- checkouts at depth 1, and push builds fetch the nearest calendar tag through a composite action
- the pull request template is gone, with the danger check that read it
- a codebase layout page with the allowed top-level entries and a check, and one page per top-level directory instead of READMEs
- the invariants in subpages, each naming the guard that checks it, with a page-length guard
- a development setup page, a coding standards page and a contributor overview
- copyable snippets come from built examples and tests, and links target explicit section anchors
- extension names in the pipeline, template and integration pages, and the breaking-changes example shows sessions
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 10, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ❌ Failed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ❌ Failed 2026-11-10T20:17:21.827519Z
We couldn’t complete this review. Comment @codex review to try again.
c45ed3b PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Warnings

Warning

This PR touches 10775 source lines across 320 files (threshold 5000). Large changes are harder to review even when well-sliced — consider splitting, or expand the description with rationale, design notes, and reviewer guidance.

✨ Highlights

  • 🧪 Existing golden tests changed (behavior likely shifted)

🧾 Changes by Scope

Scope Lines Δ% Lines Δ Lines + Lines - Files Δ Files + Files ~ Files ↔ Files -
🛠️ Source 24% 10775 7640 3135 320 33 268 5 14
🧰 Tooling 19% 8365 7600 765 61 47 10 1 3
📦 Other 15% 6625 3884 2741 76 55 11 8 2
🧪 Unit Tests 14% 6071 4850 1221 32 16 11 3 2
📄 Docs 12% 5472 4463 1009 68 25 39 1 3
📚 Examples 6% 2453 2248 205 90 57 19 13 1
⚙️ CI 3% 1325 971 354 17 5 11 - 1
🥇 Golden Tests 2% 1070 821 249 65 48 6 2 9
🔧 Toolchain 2% 1003 670 333 13 1 12 - -
🔧 Toolchain Tests 2% 772 684 88 8 3 5 - -
🏗️ Build <1% 58 41 17 2 - 2 - -
Total 100% 43989 33872 10117 752 290 394 33 35

Legend: Files + (added), Files ~ (modified), Files ↔ (renamed), Files - (removed)

🔝 Top Files

  • Doxyfile.url (Other): 2565 lines Δ (+0 / -2565)
  • libs/dom/tests/unit/Dom/Value.cpp (Unit Tests): 956 lines Δ (+13 / -943)
  • tests/unit/Extensions/AddonDiscovery.cpp (Unit Tests): 952 lines Δ (+952 / -0)

Generated by 🚫 dangerJS against c45ed3b

@codecov

codecov Bot commented Nov 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 83.44%. Comparing base (9faf2d7) to head (c45ed3b).
⚠️ Report is 1 commits behind head on develop.

Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #1374      +/-   ##
===========================================
+ Coverage    83.21%   83.44%   +0.23%     
===========================================
  Files           35       36       +1     
  Lines         3670     3861     +191     
  Branches       845      891      +46     
===========================================
+ Hits          3054     3222     +168     
- Misses         410      417       +7     
- Partials       206      222      +16     
Flag Coverage Δ
bootstrap 83.44% <ø> (+0.23%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@cppalliance-bot

Copy link
Copy Markdown

An automated preview of the documentation is available at https://1374.mrdocs.prtest2.cppalliance.org/index.html

If more commits are pushed to the pull request, the docs will rebuild at the same URL.

2026-11-10 20:20:07 UTC

@alandefreitas
alandefreitas added this pull request to the merge queue Nov 10, 2026
Merged via the queue into cppalliance:develop with commit 2d29928 Nov 11, 2026
34 of 37 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add plugins/generators via DLL

3 participants