feat: support plugins - #1256
Conversation
🧾 Changes by Scope
🔝 Top Files
|
Codecov Report✅ All modified and coverable lines are covered by tests. 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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
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 |
e42cbfa to
11bfb49
Compare
b22ac45 to
a8fc0fc
Compare
|
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. |
|
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 |
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. |
|
Done: #1262 |
|
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.
a8fc0fc to
9460dee
Compare
alandefreitas
left a comment
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Hard-coded example 👎
| @@ -0,0 +1,4 @@ | |||
| generator: stats | |||
There was a problem hiding this comment.
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[]. |
There was a problem hiding this comment.
The example is missing the implementation.
| .`plugin.cpp` | ||
| [source,cpp] | ||
| ---- | ||
| include::example$examples/generators/native/stats/plugin.cpp[tag=generator] |
There was a problem hiding this comment.
A more realistic example would be great. Although I know that's hard.
| else () | ||
| set(MRDOCS_PLUGIN_MODULES_DEFAULT ON) | ||
| endif () | ||
| option(MRDOCS_BUILD_PLUGIN_MODULES "Build the plugin example and its link test" |
There was a problem hiding this comment.
So our releases won't support plugins anyway? What's the point?
alandefreitas
left a comment
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Isn't this breaking encapsulation?
| - 'F' | ||
| - 'FMT_CONSTEVAL' | ||
| include-macros: | ||
| - 'MRDOCS_PLUGIN_*' |
There was a problem hiding this comment.
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_*.
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
PluginContext). It also providesMRDOCS_PLUGIN_MAIN, a macro that writes those exported functions for the plugin author.ENABLE_EXPORTS. It also lists, on its interface, the include directories and compile options a plugin needs.Testing
Each test checks something the others cannot.
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.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.