Skip to content

feat: support plugins - #1256

Open
gennaroprota wants to merge 4 commits into
cppalliance:developfrom
gennaroprota:feat/support_plugins
Open

feat: support plugins#1256
gennaroprota wants to merge 4 commits into
cppalliance:developfrom
gennaroprota:feat/support_plugins

Conversation

@gennaroprota

Copy link
Copy Markdown
Collaborator

This PR adds support for plugins, i.e. shared libraries written in C++ that can be written and compiled without needing Clang/LLVM headers.

Changes

  • Source:
    • A public header, mrdocs/Plugin.hpp. It says what a plugin has to export (an entry point, the interface version, a toolchain tag) and what MrDocs passes in (PluginContext). It also provides MRDOCS_PLUGIN_MAIN, a macro that writes those exported functions for the plugin author.
    • The loader. It finds the libraries in each addon root, in order, checks the version and the toolchain, and calls the entry point.
    • One call to the loader while the tool starts up, before any generator is looked up by id.
    • Export attributes. They now expand to a real attribute when MrDocs itself is built. They also move to a header of their own, because Platform.hpp includes Assert.hpp before it could define them.
  • Tests: discovery unit tests, a stats example plugin run by ctest, and a link test under tests/plugin-api.
  • Build:
    • The mrdocs target sets ENABLE_EXPORTS. It also lists, on its interface, the include directories and compile options a plugin needs.
    • The example plugin and the link test are built as module libraries. The example is built into an addon root that its own test passes to MrDocs.

Testing

Each test checks something the others cannot.

  • The discovery unit tests check which files are loaded, in what order, and that a library reachable through two roots is loaded once. Those rules are part of the documented behavior, so they should not change by accident.
  • The stats example runs MrDocs with a configuration that asks for generator: stats. That name only exists if the plugin was found, loaded, and installed its generator, so the test fails if any step in that chain breaks.
  • The link test checks that the API a plugin uses is really exported. A plugin compiles its own copy of every inline and template member, and those copies call functions that live in the tool and need an export attribute. If one of them is missing, the plugin does not link, and no other test would notice.

Writing the link test found a real problem. The comparison operators of the symbol kinds were not exported, so a plugin that sorts members could not link. The first commit fixes that.

The two ctest tests run in suites that CI already runs. The link test has no ctest entry, because it does not need one: it is a library, and a missing symbol makes the build fail. So CI needs no change.

The link test only detects a missing export on Windows, where a plugin gets its symbols from the tool's export table. On Linux and macOS, it still checks that the public headers compile the way a plugin compiles them, and that such a library links against the executable.

One thing is not tested: the cases where the loader refuses a plugin, that is, a wrong interface version, a wrong toolchain, or a missing entry point. Testing those needs a library built the wrong way on purpose.

Documentation

A new Plugins page under Extensions covers where plugins live, the entry point, the generator, and how to build one. Its code comes from the example through tag includes, so the page cannot drift from something that compiles. The plugins directory that ships with MrDocs gets a README saying what belongs in it.

Closes #58.

@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

🧾 Changes by Scope

Scope Lines Δ% Lines Δ Lines + Lines - Files Δ Files + Files ~ Files ↔ Files -
🛠️ Source 57% 1066 1001 65 31 6 25 - -
📦 Other 13% 242 240 2 5 3 2 - -
🧪 Unit Tests 11% 210 210 - 1 1 - - -
📚 Examples 10% 195 195 - 5 4 1 - -
📄 Docs 7% 134 124 10 3 1 2 - -
🏗️ Build 1% 13 13 - 2 - 2 - -
Total 100% 1860 1783 77 47 15 32 - -

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

🔝 Top Files

  • include/mrdocs/Plugin.hpp (Source): 295 lines Δ (+295 / -0)
  • src/mrdocs/Support/PluginLoader.cpp (Source): 263 lines Δ (+263 / -0)
  • tests/unit/Support/PluginLoader.cpp (Unit Tests): 210 lines Δ (+210 / -0)

Generated by 🚫 dangerJS against 9460dee

@codecov

codecov Bot commented Aug 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 83.12%. Comparing base (e31308f) to head (9460dee).

Additional details and impacted files
@@           Coverage Diff            @@
##           develop    #1256   +/-   ##
========================================
  Coverage    83.12%   83.12%           
========================================
  Files           35       35           
  Lines         3662     3662           
  Branches       844      844           
========================================
  Hits          3044     3044           
  Misses         410      410           
  Partials       208      208           
Flag Coverage Δ
bootstrap 83.12% <ø> (ø)

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

cppalliance-bot commented Aug 11, 2026

Copy link
Copy Markdown

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

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

2026-08-21 10:34:00 UTC

@gennaroprota gennaroprota changed the title Feat/support plugins feat: support plugins Aug 12, 2026
@gennaroprota
gennaroprota force-pushed the feat/support_plugins branch 3 times, most recently from b22ac45 to a8fc0fc Compare August 12, 2026 15:05
@alandefreitas

Copy link
Copy Markdown
Collaborator

I think the doc preview isn't working. We need to debug that for this PR. In any case, I read the adoc file, and I'm not sure I understand the user workflow. As far as I understand, the user would need an mrdocs installation and a way to link whatever they need to their own project to write a plugin. And tests would have to emulate that. This can't be tested from the inside before mrdocs is installed.

@gennaroprota

Copy link
Copy Markdown
Collaborator Author

It seems that the doc preview stopped working around 5-6 June 2026.

About the user workflow, your reading is right: the author needs an install. But I'm not sure what you mean by "can't be tested from the inside"; isn't that what tests/plugin-api does?

However, I noticed that the docs prescribe find_package(mrdocs REQUIRED CONFIG) in two places, which fails for everyone. The generated config expands @DUKTAPE_PACKAGE_NAME@, which is now undefined, into a bare find_dependency(), which is a hard CMake error, so both sets of instructions have been broken the whole time. find_dependency(fmt) is dead too: nothing in the project links fmt any more. That leaves LLVM and Clang, legitimate for mrdocs-core consumers, but not for plugin authors. I will fix the config here, and add an install-and-consume test that builds a plugin against a staged prefix alone.

@alandefreitas

alandefreitas commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

I will fix the config here and add an install-and-consume test that builds a plugin against a staged prefix alone.

Please hang on a little bit then, because I've been already working on precisely this issue for the last two weeks. My fork is nothing but CI tests for test/cmake-package lately.

@alandefreitas

Copy link
Copy Markdown
Collaborator

Done: #1262

@gennaroprota

Copy link
Copy Markdown
Collaborator Author

Great!

`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.
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.

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

@alandefreitas alandefreitas left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really nice. Especially that we can just use the same interface as the extensions.

What was the design process? How is the design compared to, for instance, clang plugins?

It's also really nice that it has a system to check whether the versions are incompatible. How do we avoid extreme incompatibility between versions? At the very least, every time we update LLVM, the plugins will have an API breaking change. In practice, it seems like almost new Mr.Docs commit will imply in a breaking change that invalidates other plugins. A way to fix that would be maybe a more stable class that reconstructs the objects on the other side but I'm not sure if that's feasible. The macros delegating to an intermediary C API can also help some more but the structs can still break anyway.

Were all of these MRDOCS_DECL wrong? What happened down there?


A plugin is a shared library that Mr.Docs loads as it starts up, handing it a context. Through that context, the plugin installs what it provides, so the rest of the run sees it as if it had shipped with the tool: generators, which turn the corpus into output, and transforms, which change the corpus before any generator sees it.

Reach for a plugin when the output needs {cpp}: a format whose rules are awkward to express in templates, a generator that depends on a system library, or one whose work is heavy enough to justify compiled code. The lighter options remain xref:extensions/data-driven-generators.adoc[data-driven generators] (templates only) and script-driven generators (Lua or JavaScript, see xref:extensions/corpus-extensions.adoc[Extensions]). The difference from xref:extensions/as-library.adoc[Mr.Docs as a library] is that a plugin runs inside the ordinary `mrdocs` tool: you keep the command line, the configuration, and everything else Mr.Docs does, and you do not build Mr.Docs or LLVM.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another interesting use case is linking C/C++ libraries. For instance, a plugin could use sqlite or something like that.


== Where plugins live

Mr.Docs looks in the `plugins` subdirectory of every addon root, that is, under both the xref:configuration/reference.adoc#addons_option[`addons`] directory and each xref:configuration/reference.adoc#addons-supplemental_option[`addons-supplemental`] directory, in that order:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sections seem a bit out of order. I've been following this page trying to build a plugin to test the system but the section on how to build it is the last section and this is already about how to place it. The first step is always setting up the directories and the boilerplate. That can't be a detail left to the end.


== The entry point

The `MRDOCS_PLUGIN_MAIN` macro defines the function Mr.Docs calls, naming the cpp:PluginContext[] parameter it receives:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The macro needs to be linked to the reference documentation.


The context is how a plugin reaches Mr.Docs: cpp:PluginContext::installGenerator[] adds a generator, cpp:PluginContext::installTransform[] adds a transform, and cpp:PluginContext::config[] returns the configuration the run was started with, already loaded and normalized, in case what the plugin installs depends on it. The reference is valid for the duration of the call.

Along with the entry point, the macro defines a function that reports the version of the plugin interface the library was compiled against. Mr.Docs compares it with its own and refuses to call a plugin that was built against a different one. A plugin therefore has to be rebuilt when it is used with a Mr.Docs whose plugin interface has changed.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we mention the macro for the plugin version be mentioned and linked here? And how does the versioning system work, etc? How do we developers ensure that this MRDOCS_PLUGIN_API_VERSION is always correct?

# so the library is built into one. The generator expression keeps CMake
# from appending a per-configuration subdirectory, which would put the
# library somewhere the test cannot name.
set(MRDOCS_STATS_ADDONS "${CMAKE_CURRENT_BINARY_DIR}/native/stats/addons")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

examples/generators/native? We never see this term "native" anywhere else, and the other generators in examples/generators are probably more "native" than this one. Maybe example/plugins/stats?


[source,cpp]
----
class BriefFiller

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hard-coded example 👎

@@ -0,0 +1,4 @@
generator: stats

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this example. Where's the project being documented with the plugin doing something with it? Is this the project that defines the plugin or the example that uses the plugin? In either case, where's the other project?

}
----

The transforms a plugin installs run before the ones an extension script registers with `mrdocs.register_transform`, and among themselves in the order they were installed. An error from a transform stops the run before any generator sees the corpus, and the diagnostic names the transform through cpp:Transform::id[].

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example is missing the implementation.

.`plugin.cpp`
[source,cpp]
----
include::example$examples/generators/native/stats/plugin.cpp[tag=generator]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A more realistic example would be great. Although I know that's hard.

Comment thread CMakeLists.txt
else ()
set(MRDOCS_PLUGIN_MODULES_DEFAULT ON)
endif ()
option(MRDOCS_BUILD_PLUGIN_MODULES "Build the plugin example and its link test"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So our releases won't support plugins anyway? What's the point?

@alandefreitas alandefreitas left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry. I clicked on submit review by mistake before finishing it.

A plugin must match the compiler major, the standard library, and on MSVC the iterator debug level and CRT, and so on and so on. Do we also have a system in place to check all of that? That's the C boundary thing I was suggesting maybe could work. I'm not sure how other systems do it. In any case, nothing in the docs or the release notes says what that is. The person downloading mrdocs after this commit won't have access to plugins because cmakelists.txt disables it for -static builds (removing -static breaks the release across OS versions by the way - this is there for a reason).

MRDOCS_DECL now means dllexport whenever MRDOCS_TOOL is defined, even in a static build? Is this correct?

Can't the tests easily check the path where MRDOCS_PLUGIN_API_VERSION isn't accepted?

#elif defined(_MSC_VER)
# if defined(MRDOCS_TOOL)
#if defined(_MSC_VER)
# if defined(MRDOCS_TOOL) // building MrDocs

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this breaking encapsulation?

Comment thread docs/mrdocs.yml
- 'F'
- 'FMT_CONSTEVAL'
include-macros:
- 'MRDOCS_PLUGIN_*'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mmm... Our convention for macros is bad. It was bad and it remains bad. It's more than excluding MRDOCS_* and it's more than includeing MRDOCS_PLUGIN_* but it's less than includeing MRDOCS_*.

@alandefreitas alandefreitas mentioned this pull request Sep 3, 2026
6 tasks
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