Defining Extensions
A pipeline extension is code that Mr.Docs loads as it starts up. It registers corpus transforms and generators, and the rest of the run sees them as if they had shipped with the tool. You can write an extension in JavaScript, Lua, or C++:
-
A script is a
.jsfile that the embedded JerryScript engine runs, or a.luafile that the embedded Lua engine runs. It sees the corpus through DOM objects. -
A C++ plugin is a native shared library that Mr.Docs loads. It sees the real
Symbol,CorpusandConfigtypes and can call the C++ helpers of Mr.Docs.
The tables below compare these languages according to various criteria. The section Choosing between JavaScript, Lua and C++ has the measurements behind the conclusions.
| JavaScript | Lua | C++ | |
|---|---|---|---|
Familiarity |
|
|
|
Build step |
|
|
|
Survives a Mr.Docs release |
|
|
|
Real C++ types |
|
|
|
| JavaScript | Lua | C++ | |
|---|---|---|---|
Speed |
|
|
|
Memory |
|
|
|
Out of memory |
|
|
|
| JavaScript | Lua | C++ | |
|---|---|---|---|
Standard library |
|
|
|
Other libraries |
|
|
|
Debugging |
|
|
|
Engine and version |
JerryScript 3.0.0, |
Lua 5.4.8 |
Mr.Docs itself |
Extension locations
Mr.Docs looks for extensions in the extensions/ directory of every addon root:
| Language | File in extensions/ |
Entry file of a directory |
|---|---|---|
JavaScript |
|
|
Lua |
|
|
C++ |
|
|
For example, a plugin with a dependency of its own sits in a directory, under the name CMake gives a module library:
my-addons/extensions/
└── report/
├── libreport.so
└── libsqlite3.so.0
|
|
As usual, you can use the addons-supplemental option to register more addon directories with extensions. An extension is named by its file name without the suffix and without the lib prefix of a native library, or by the name of its directory.
|
Load order
An extension must not rely on the order in which Mr.Docs loads extensions. A transform works on the corpus it receives and must not assume that another transform ran before it or will run after it. Scripts and plugins can always interleave. |
Registering an extension
An extension registers transforms, generators, or both, and Mr.Docs can call each one later in the run, once, with the run context. A script calls a function of the mrdocs object, and a plugin calls a member of the context its entry point receives:
| Language | Registers a transform | Registers a generator |
|---|---|---|
JavaScript, Lua |
|
|
C++ |
|
|
The hello-world transform below reports one line, "Hello from a transform". A script calls mrdocs., and a plugin calls register_transform(id, fn)plugin. from its entry point, the body of registerTransform(id, fn)MRDOCS_PLUGIN_MAIN:
-
JavaScript
-
Lua
-
C++
addons/extensions/hello_world_transform.jsmrdocs.register_transform("hello-world-transform", function(ctx) {
ctx.report.warn("Hello from a transform");
});
addons/extensions/hello_world_transform.luamrdocs.register_transform("hello-world-transform", function(ctx)
ctx.report.warn("Hello from a transform")
end)
hello-world-transform/plugin.cpp#include <mrdocs/Extensions/TransformExtensionContext.hpp>
#include <mrdocs/Plugin.hpp>
#include <mrdocs/Support/Reporter.hpp>
#include <mrdocs/Transform.hpp>
MRDOCS_PLUGIN_MAIN(plugin)
{
return plugin.registerTransform("hello-world-transform",
[](mrdocs::TransformExtensionContext const& ctx) -> mrdocs::Expected<void>
{
ctx.report->warn("Hello from a transform");
return {};
});
}
The hello-world generator writes one file, hello.txt, with one line. A script writes it through ctx.output.write, and a plugin writes it under ctx.outputPath:
-
JavaScript
-
Lua
-
C++
addons/extensions/hello_world_generator.jsmrdocs.register_generator("hello-world-generator", function(ctx) {
ctx.output.write("hello.txt", "Hello from a generator\n");
});
addons/extensions/hello_world_generator.luamrdocs.register_generator("hello-world-generator", function(ctx)
ctx.output.write("hello.txt", "Hello from a generator\n")
end)
hello-world-generator/plugin.cpp#include <mrdocs/Extensions/GeneratorExtensionContext.hpp>
#include <mrdocs/Generator.hpp>
#include <mrdocs/Plugin.hpp>
#include <filesystem>
#include <fstream>
MRDOCS_PLUGIN_MAIN(plugin)
{
return plugin.registerGenerator("hello-world-generator",
[](mrdocs::GeneratorExtensionContext const& ctx) -> mrdocs::Expected<void>
{
std::filesystem::path const dir(ctx.outputPath);
std::error_code ec;
std::filesystem::create_directories(dir, ec);
std::ofstream os(dir / "hello.txt");
if (ec || !os)
{
return mrdocs::Unexpected(mrdocs::Error(
"could not write hello.txt"));
}
os << "Hello from a generator\n";
return {};
});
}
Every transform and generator has an id. The rules for it are in Registering a transform and Registering a generator. An extension can register any number of transforms and generators.
|
Input Parameters
A script gets the context as |
Building a C++ plugin
A C++ plugin runs inside the ordinary mrdocs tool. You can build one with the following recipe:
-
Build a CMake module library that links
mrdocs::plugin-api. -
Write the entry point with
MRDOCS_PLUGIN_MAIN. -
Register a generator, a transform, or both. Start from the hello-world plugins.
-
Install the library under
extensions/of an addon root. -
Rebuild it against the Mr.Docs that will load it, as what has to match says.
The CMake target
A plugin is a CMake module library that links mrdocs::plugin-api, the target the installed package defines for plugins.
A module library is CMake’s name for a shared library that no other target will link against. Only a program can load a module at run time.
-
On Windows, CMake writes no import library for it.
-
On macOS, it writes a bundle that ends in
.so. -
On Linux, it’s the same as a shared library.
The package ships mrdocs_add_plugin, which does all of the setup:
find_package(mrdocs REQUIRED)
mrdocs_add_plugin(my_plugin src/plugin.cpp)
The function creates the library, links it, and compiles it with hidden visibility, so that its own symbols stay its own, as the following recipe does. It is analogous to:
find_package(mrdocs REQUIRED)
add_library(my_plugin MODULE src/plugin.cpp)
target_link_libraries(my_plugin PRIVATE mrdocs::plugin-api)
set_target_properties(my_plugin PROPERTIES
CXX_VISIBILITY_PRESET hidden
VISIBILITY_INLINES_HIDDEN ON)
|
Linking against the plugin API
In a static build, the platform decides how the undefined symbols are checked:
The dynamic loader resolves the undefined symbols against the export table of the CMake links the plugin with the CMake links the plugin against the import library that it writes for Without hidden visibility, a call from a plugin to one of its own functions can bind to a function of the same name in the Mr.Docs executable. The entry points stay visible, since |
The entry point
Use the MRDOCS_PLUGIN_MAIN macro once, in one translation unit of the plugin. The body that follows it receives the PluginContext parameter and returns an Expected<void>: {} on success, or an Error to refuse the plugin. The error stops the run with a message that names the plugin. This body registers a generator:
count-by-kind/generator/plugin.cppMRDOCS_PLUGIN_MAIN(plugin)
{
return plugin.registerGenerator(
"stats", writeStats, "Symbol statistics", "txt");
}
|
What the macro defines
Along with the entry point, the macro defines functions that report how the library was built. Mr.Docs calls them before it calls the plugin to make sure the plugin was compiled with compatible settings.
|
Installing a plugin
Put the library under extensions/ of an addon root, as a file or in a directory of its own, as the Extension locations section describes. A plugin that depends on shared libraries of its own should live in a directory and keep its dependencies there.
On Linux and macOS, mrdocs_add_plugin records the run path, so that these dependencies can be found. By hand, that is:
set_target_properties(my_plugin PROPERTIES BUILD_WITH_INSTALL_RPATH ON)
if (APPLE)
set_target_properties(my_plugin PROPERTIES INSTALL_RPATH "@loader_path")
elseif (UNIX)
set_target_properties(my_plugin PROPERTIES INSTALL_RPATH "$ORIGIN")
endif ()
Plugin compatibility
Build the plugin against the Mr.Docs installation you will run it with, with the standard library that Mr.Docs uses, and rebuild it when you update Mr.Docs. In some cases, this might mean building Mr.Docs from source with the bootstrap script so that plugins and Mr.Docs are compatible on your platform.
| Must match | Where it comes from |
|---|---|
The exact Mr.Docs build |
|
The standard library and its settings |
|
On each platform, the plugin’s toolchain has to fit the one that built Mr.Docs. The installed package records the macOS deployment target and the MSVC runtime it was built with. At configure time, mrdocs_add_plugin compares them with the plugin’s own settings and stops with a message naming both values when they differ. The loader compares the build id and the standard library tag when the plugin loads. A Mr.Docs built without a build id refuses every plugin, so when you build it from a source archive without .git, pass -DMRDOCS_BUILD_ID=<id> to CMake.
The compiler stays your choice on Linux. The tag holds the library’s ABI settings, not its release. A plugin fails in the loader only when it needs a libstdc++ symbol version newer than the one the release bundles. The Linux release carries the libstdc++ of GCC 15, whose newest version is GLIBCXX_3.4.34.
Choosing between JavaScript, Lua and C++
Pick the language by what the extension has to do and how long it has to last. The tables in the introduction compare them, and the subsections give the measurements behind the script columns.
-
Pick JavaScript when its authors already write JavaScript, or when the work is a few lookups per run. It has closures, destructuring, regular expressions and classes, and more people read it at a glance than read Lua. You get the language and its built-ins:
JSON, regular expressions,Map,Set,Promiseand the array methods. It handles any corpus that fits in 4 GB, which is enough for most real codebases. -
Pick Lua, the language designed to be embedded, when the script runs over a large codebase, keeps many symbols alive, or runs on every build. It has no memory limit and runs faster for the same work. It also includes the full Lua standard library, making it a good choice for scripts you reuse across many projects.
-
Pick C++ when you can rebuild the extension for each Mr.Docs release and it needs one of these:
-
Work heavy enough to justify compiled code.
-
A generator that links to a library of its own, such as SQLite to write the corpus into a database, or a PDF library to typeset the reference.
-
Other tools cover different needs. If Handlebars templates are enough, use data-driven generators. If a program has to drive extraction itself, use Mr.Docs as a library.
Whichever script language you pick, the functionality and the symbol fields in the DOM reference work the same.
Performance
The table below compares the same transforms written once in JavaScript and once in Lua, run over the Mr.Docs corpus:
| Task | Lua | JavaScript | Ratio |
|---|---|---|---|
Count symbols |
3 ms |
7 ms |
2× |
Collect names |
4 ms |
15 ms |
4× |
Read a nested property |
4 ms |
32 ms |
8× |
Build text per symbol |
4 ms |
66 ms |
15× |
Keep every symbol in an array |
1 ms |
19 ms |
17× |
Number loop, no symbols involved |
17 ms |
157 ms |
9× |
Each transform timed itself from inside the script, so extraction and rendering are excluded. The figures are the average of 30 runs. The scripts and the runner live in tests/benchmarks/script-engines/, and cmake --build build/<preset> --target mrdocs-benchmark-script-engines prints the table for your machine.
Lua wins every row:
-
The number loop, which touches no symbols, shows the interpreters themselves: Lua’s register machine with native integers runs about nine times faster than JerryScript’s stack machine with 32-bit tagged values.
-
On the symbol tasks, the gap is smaller where a script only reads, and larger where it builds things.
-
Reading a property costs about a microsecond in JavaScript, because every read goes through a
Proxytrap that converts the property name and locks the engine. Lua does the same work through a metatable on a small userdata, at a fraction of the cost. -
Building a string one piece at a time is where JavaScript falls furthest behind: JerryScript copies on every concatenation, while Lua’s
table.concatbuilds the result once. -
For a corpus of 100,000 symbols, a JavaScript transform that reads two properties of every symbol takes well under a second. The same Lua transform takes a tenth of that.
Symbols as values
In both engines ctx.corpus.symbols is lazy. Reading it costs nothing, and each element becomes a script value when the script reaches it. A loop over every symbol of any corpus works, and so does keeping the ones you filtered out. On the Mr.Docs corpus, keeping 102,624 JavaScript symbol proxies alive at once took 129 ms and added 5.4 MB, about 53 bytes each. Lua kept every symbol in under 600 KB.
Each read builds a fresh value, in both engines, so two reads of the same element are not the same object. Compare symbols by id, not by identity. Do not rely on indexOf, includes or a Set to find or remove duplicate symbol objects in JavaScript, or on table keys in Lua.
Memory
Lua stores native (typically 64-bit) pointers and takes every value from the system allocator, one call at a time. Its values can live anywhere in memory, so there is no block to run out of, and Lua frees a value when its garbage collector finds it is no longer used. A Lua script is therefore limited by the machine’s memory, like the rest of Mr.Docs.
If an allocation does fail, Lua raises a "not enough memory" error that a script can catch with pcall. If the script does not catch it, the error ends the transform with that message.
JerryScript stores 32-bit offsets into one block instead of native pointers, so each object costs less, and that block is where its values live. The block grows as a script creates objects and can reach just under 4 GB, the most a 32-bit offset can address. A script that keeps 50 MB alive costs about 50 MB.
If a script exhausts the block, the engine stops and Mr.Docs exits with an error that names the size. A script cannot catch it, since there is no JavaScript exception for this case.
Standard libraries
-
JavaScript
-
Lua
-
C++
The engine implements the language and the ECMAScript built-ins, and nothing beyond that.
-
It provides
Object,Array,String,Number,BigInt,Math,JSON,RegExp,Date,Map,Set,WeakMap,WeakRef,Promise,Symbol,Proxy,Reflect,ArrayBufferwith the typed arrays, generators, andasyncfunctions. -
It does not provide
Intl,setTimeout,queueMicrotask,TextEncoder,fetch,structuredClone, or anything Node or a browser would add (fs,path,process,require). -
There is no module system:
importandexportare syntax errors, so a script is one file. A JavaScript extension in a directory of its own, such asextensions/rename/index.js, is that one file too. -
Mr.Docs adds host objects:
console, withloganderror, andmrdocs, withregister_transform,register_generatorandreport. Everything else a script needs comes through thectxobject it is handed.
Mr.Docs opens the whole standard library: string, table, math, io, os, utf8, coroutine, debug and package. require finds those by name.
-
A Lua extension that is a directory of its own, such as
extensions/conventions/init.lua, can alsorequirethe modules next to its entry file. Mr.Docs looks in that directory before it triespackage.path, for<name>.luaand then<name>/init.lua. -
That is how
require "rules"findsconventions/rules.lua, even when the path to the directory has non-ASCII characters. -
A script that is a file directly in
extensions/has Lua’s defaultpackage.path: system paths and the current directory. Keep it in one file, or move it into a directory of its own.
A plugin uses the C++ standard library of the toolchain that built it, and that library has to match the one of Mr.Docs (see what has to match). It can link any other library, which goes in the directory of the plugin, as Installing a plugin describes.