Plugins
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 you need C++: a format whose rules are awkward to express in templates, work heavy enough to justify compiled code, or a generator that links to a C or C++ library on its own (e.g. SQLite to write the corpus into a database another tool queries, or a PDF library to typeset the reference). The lighter options remain data-driven generators (templates only) and script generators (Lua or JavaScript). The difference from 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.
The stats example is the whole of what follows, as a directory you can copy: the plugin, and a small project documented with it.
Building a plugin
A plugin is a CMake module library that links the mrdocs tool:
find_package(mrdocs REQUIRED CONFIG)
add_library(stats MODULE plugin.cpp)
target_link_libraries(stats PRIVATE mrdocs::mrdocs)
target_compile_features(stats PRIVATE cxx_std_23)
Linking the tool is what resolves the Mr.Docs symbols the plugin calls, and it is the whole build dependency: no LLVM, no Clang, no separate library to ship alongside. That holds because the public headers only ever forward-declare the LLVM types they name; LLVM comes back only for a plugin that reaches past them into Mr.Docs’s internal headers, as one supplying its own compilation database would.
Importing the package carries a separate condition. A Mr.Docs built the default way bundles its private dependencies, so find_package needs nothing besides the installation. One built to re-export them instead, which the package config reports as MRDOCS_BUILT_REEXPORT_DEPENDENCIES, sends find_package looking for LLVM and Clang whether or not the plugin ever names them.
The resulting library has to be built the way the Mr.Docs that will load it was built. That requirement is checked rather than assumed, and the section on keeping a plugin loadable is about what it covers.
The entry point
The MRDOCS_PLUGIN_MAIN macro defines the function Mr.Docs calls, naming the PluginContext parameter it receives. This plugin installs one of each thing a plugin can install:
plugin.cppMRDOCS_PLUGIN_MAIN(context)
{
MRDOCS_TRY(context.installGenerator(std::make_unique<StatsGenerator>()));
return context.installTransform(std::make_unique<BriefFiller>());
}
The context is how a plugin reaches Mr.Docs: PluginContext::installGenerator adds a generator, PluginContext::installTransform adds a transform, and 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 two functions that report how the library was built, and Mr.Docs calls both before it calls the plugin. One reports MRDOCS_PLUGIN_API_VERSION, the version of the interface; the other reports the toolchain. Neither is written by hand, so none of the three can disagree with each other.
The generator
A plugin’s generator is an ordinary Generator subclass: the same interface the built-in formats implement, and the same one the library examples use.
plugin.cpp// A generator that writes one line per symbol kind, indicating the kind and
// how many symbols of that kind the corpus has.
class StatsGenerator final
: public mrdocs::Generator
{
public:
std::string_view
id() const noexcept override
{
return "stats";
}
std::string_view
displayName() const noexcept override
{
return "Symbol statistics";
}
std::string_view
fileExtension() const noexcept override
{
return "txt";
}
mrdocs::Expected<void>
build(
mrdocs::Corpus const& corpus,
mrdocs::Config const& config) const override;
};
Generator::build receives the corpus and the configuration, and owns everything from there: where its files go and what goes in them. This one counts the symbols by kind and writes a single file:
plugin.cppmrdocs::Expected<void>
StatsGenerator::
build(
mrdocs::Corpus const& corpus,
mrdocs::Config const& config) const
{
std::map<std::string_view, int> const counts =
countByKind(corpus);
std::filesystem::path const dir = outputDir(config);
std::error_code ec;
std::filesystem::create_directories(dir, ec);
std::filesystem::path const file = dir / "stats.txt";
mrdocs::Expected<void> result;
std::ofstream os(file);
if (!os)
{
result = mrdocs::Unexpected(mrdocs::formatError(
"could not open \"{}\" for writing", file.string()));
}
else
{
for (auto const& [kind, count] : counts)
{
os << kind << ' ' << count << '\n';
}
}
return result;
}
Transforming the corpus
A plugin can also install a Transform: a pass that runs once, after the corpus is built and finalized and before any generator runs, so what it changes is what every output format sees. Where a generator is handed the corpus as const, a transform is handed it mutable, and may read it, change the symbols it finds, or both. What it cannot do is create a symbol or destroy one, since a corpus keeps its storage to itself. Everything a symbol holds is in reach, though, including the lists of members that the generators walk to decide what to write.
plugin.cpp// A transform that gives a symbol with no brief a placeholder one, so that
// the gap shows up in the output instead of a blank space.
class BriefFiller final
: public mrdocs::Transform
{
public:
std::string_view
id() const noexcept override
{
return "brief-filler";
}
mrdocs::Expected<void>
apply(
mrdocs::Corpus& corpus,
mrdocs::Config const& config) const override;
};
The example’s transform gives a symbol with no brief a placeholder one, so that the gap shows up in the output instead of a blank space:
plugin.cpp// Give a symbol a brief if it has none.
void
fillBrief(mrdocs::Symbol& symbol)
{
if (!symbol.doc)
{
symbol.doc.emplace();
}
mrdocs::DocComment& doc = symbol.doc.value();
if (!doc.brief)
{
doc.brief = mrdocs::doc::BriefBlock("Undocumented.");
}
}
mrdocs::Expected<void>
BriefFiller::
apply(
mrdocs::Corpus& corpus,
mrdocs::Config const&) const
{
// A corpus is walked as const. Changing a symbol means asking for it
// by id, which is where the corpus hands out a mutable reference.
for (mrdocs::Symbol const& symbol : corpus)
{
if (mrdocs::Symbol* const target = corpus.find(symbol.id))
{
fillBrief(*target);
}
}
return {};
}
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 Transform::id.
Installing a plugin
Mr.Docs looks in the plugins subdirectory of every addon root, that is, under both the addons directory and each addons-supplemental directory, in that order:
my-addons/
└── plugins/
└── stats.dll (1)
| 1 | .dll on Windows, .so on Linux, .dylib or .so on macOS. |
Every library in such a directory is loaded, in name order within a root, and roots are visited in the order the configuration lists them. One library reachable through several roots is loaded once. Anything that is not a library is ignored, so a directory can hold a README, an import library, or debug information without confusing the search.
A library that cannot be loaded, does not export the entry points, or reports an error of its own stops the run.
Once the library is there, the generator it installs is selectable like any other, through the generator option:
generator: stats
For the sample project that comes with the example, whose input describes a handful of symbols, the generator writes:
stats.txtenum 1
enum-constant 2
function 2
namespace 2
record 1
typedef 1
variable 1
Keeping a plugin loadable
A plugin and Mr.Docs pass C++ objects between them: the plugin allocates a generator that Mr.Docs destroys, and both inline the standard library types the API exposes. Two things have to hold for that, and Mr.Docs checks both before it calls a plugin, so a mismatch is a diagnostic naming the library rather than a crash somewhere unrelated.
The first is the interface itself. MRDOCS_PLUGIN_API_VERSION names the version of the plugin API, and its value changes whenever that API changes, so a plugin is rebuilt against the headers of the Mr.Docs it is to be loaded by. Note that the plugin API is a small part of the Mr.Docs headers, and only the part a plugin sees: the context, the generator and transform interfaces, and the corpus and configuration they are handed. Changes elsewhere leave it alone.
The second is the toolchain. The two sides have to be built with the same compiler and standard library, in a configuration that lays those types out the same way, so the plugin reports what it was built with: the compiler and its major version, the standard library, and the settings that move those layouts, which on MSVC are the iterator debug level and how the runtime is linked. Mr.Docs refuses a plugin whose report differs from its own.
One configuration rules plugins out altogether: a statically linked Mr.Docs. Such a build has no dependable way to load a shared library while it runs, and cannot produce one either, since the startup files a static link brings in carry relocations a shared object cannot hold. The package config reports whether an installation can have plugins at all as MRDOCS_BUILT_PLUGIN_MODULES, so a build can ask before it tries.
That is not hypothetical for the published binaries: the Linux release is linked statically, so that one binary runs across distributions, and therefore cannot load a plugin. The Windows and macOS releases are not, and can. A Mr.Docs you build yourself takes plugins on every platform, since the static link is a choice the Linux release makes rather than a default.