Skip to content

Commit adf45de

Browse files
committed
plugins: support nix_plugin_entry, do some minor reworks
Plugins are already allowed to fail to load, let's be even more tolerant of them failing to load by ignoring inaccessible ones with a warning. This change also significantly reworks the docs for plugins. This is a tiny extremely partial backport of the C API at NixOS/nix#8699. Fixes: https://git.lix.systems/lix-project/lix/issues/740 CC: https://git.lix.systems/lix-project/lix/issues/359 Change-Id: If4ee20c3daaf26c8184a415eef3e20ca5b5e7aef
1 parent ccab8cd commit adf45de

File tree

5 files changed

+76
-26
lines changed

5 files changed

+76
-26
lines changed

doc/manual/change-authors.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,9 @@ winter:
208208
xanderio:
209209
github: xanderio
210210

211+
yorickvp:
212+
github: yorickvp
213+
211214
yshui:
212215
github: yshui
213216

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
synopsis: "Add `nix_plugin_entry` entry point for plugins"
3+
issues: [fj#740, fj#359]
4+
prs: [gh#8699]
5+
cls: [2826]
6+
category: Development
7+
credits: ["jade", "yorickvp"]
8+
---
9+
10+
Plugins are an exceptionally rarely used feature in Lix, but they are important as a prototyping tool for code destined for Lix itself, and we want to keep supporting them as a low-maintenance-cost feature.
11+
As part of the overall move towards getting rid of static initializers for stability and predictability reasons, we added an explicit `nix_plugin_entry` function like CppNix has, which is called immediately after plugin load, if present.
12+
This makes control flow more explicit and allows for easily registering things that have had their static initializer registration classes removed.

lix/libstore/globals.cc

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,10 @@ Paths PluginFilesSetting::parse(const std::string & str, const ApplyConfigOption
357357
return BaseSetting<Paths>::parse(str, options);
358358
}
359359

360+
// C++ syntax so weird that it breaks the tree-sitter highlighter!
361+
// *Technically* the C linkage function pointer should be so annotated.
362+
// Does it actually matter? Almost certainly not!
363+
extern "C" using NixPluginEntry = void (*)();
360364

361365
void initPlugins()
362366
{
@@ -365,20 +369,36 @@ void initPlugins()
365369
Paths pluginFiles;
366370
try {
367371
auto ents = readDirectory(pluginFile);
368-
for (const auto & ent : ents)
372+
for (const auto & ent : ents) {
369373
pluginFiles.emplace_back(pluginFile + "/" + ent.name);
374+
}
370375
} catch (SysError & e) {
371-
if (e.errNo != ENOTDIR)
372-
throw;
376+
if (e.errNo != ENOTDIR) {
377+
// I feel like it is more reasonable to skip plugins if they are
378+
// inaccessible, since it is *already* the case that plugins
379+
// are not guaranteed to load due to version mismatches etc
380+
// causing dlopen failures.
381+
warn("could not access plugin file '%s', skipping it: %s", pluginFile, e.msg());
382+
continue;
383+
}
373384
pluginFiles.emplace_back(pluginFile);
374385
}
375386
for (const auto & file : pluginFiles) {
376387
/* handle is purposefully leaked as there may be state in the
377388
DSO needed by the action of the plugin. */
378389
void *handle =
379390
dlopen(file.c_str(), RTLD_LAZY | RTLD_LOCAL);
380-
if (!handle)
381-
warn("could not dynamically open plugin file '%s': %s", file, dlerror());
391+
if (!handle) {
392+
warn("could not dynamically open plugin file '%s', skipping it: %s", file, dlerror());
393+
continue;
394+
}
395+
396+
/* Older plugins use a statically initialized object to run their code.
397+
Newer plugins can also export nix_plugin_entry() */
398+
auto nix_plugin_entry = reinterpret_cast<NixPluginEntry>(dlsym(handle, "nix_plugin_entry"));
399+
if (nix_plugin_entry) {
400+
nix_plugin_entry();
401+
}
382402
}
383403
}
384404

lix/libstore/settings/plugin-files.md

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,31 @@ internalName: pluginFiles
44
settingType: PluginFilesSetting
55
default: []
66
---
7-
A list of plugin files to be loaded by Nix. Each of these files will
8-
be dlopened by Nix, allowing them to affect execution through static
9-
initialization. In particular, these plugins may construct static
10-
instances of RegisterPrimOp to add new primops or constants to the
11-
expression language, RegisterStoreImplementation to add new store
12-
implementations, RegisterCommand to add new subcommands to the `nix`
13-
command, and RegisterSetting to add new nix config settings. See the
14-
constructors for those types for more details.
15-
16-
Warning! These APIs are inherently unstable and may change from
17-
release to release.
18-
19-
Since these files are loaded into the same address space as Nix
20-
itself, they must be DSOs compatible with the instance of Nix
21-
running at the time (i.e. compiled against the same headers, not
22-
linked to any incompatible libraries). They should not be linked to
23-
any Lix libs directly, as those will be available already at load
24-
time.
25-
26-
If an entry in the list is a directory, all files in the directory
27-
are loaded as plugins (non-recursively).
7+
A list of plugin files to be loaded by Lix.
8+
9+
Each of these files will be `dlopen`ed by Lix, allowing them to affect execution by registering various entities in Lix.
10+
After the plugins are loaded, they will have the function within them with signature `extern "C" void nix_plugin_entry(void)` called if it is defined.
11+
12+
If an entry in the list is a directory, all files in the directory are loaded as plugins (non-recursively).
13+
14+
FIXME(jade): We should provide a `nix_plugin_finalize()` that gets called at some point in teardown for use cases like nix-otel which need to be able to cleanup, flush things to network, etc, on exit without having to do that from life-after-main().
15+
16+
In particular, these plugins may:
17+
- Construct static instances of `RegisterPrimOp` to add new primops or constants to the expression language (FIXME: will be replaced with an explicit function).
18+
- Add new store implementations with `StoreImplementations::add`.
19+
- Construct static instances of `RegisterCommand` to add new subcommands to the `nix` command (FIXME: will be replaced with an explicit function).
20+
- Construct static instances of `Setting` to add new Lix settings (FIXME: will be replaced with an explicit function).
21+
22+
See the documentation for those symbols for more details.
23+
Note all the FIXMEs above: Lix is removing its usages of static initializers, see <https://git.lix.systems/lix-project/lix/issues/359>.
24+
25+
Warning! These APIs are inherently unstable and may change in minor versions.
26+
It's recommended to, if you *are* relying on Lix's unstable C++ API, develop against Lix `main`, run `main` yourself, and be active in Lix development.
27+
28+
Since these files are loaded into the same address space as Lix itself, they must be DSOs compatible with the instance of Lix running at the time (i.e. compiled against the same headers, not linked to any incompatible libraries, produced by the same nixpkgs).
29+
30+
It's recommended that this setting *not* be used in `nix.conf` since it is almost always the case that there are multiple versions of the Nix implementation on a machine.
31+
In particular, CppNix (still true in 2.26 as of this writing) considers plugin load failure to be a hard error unlike Lix (since pre-2.90), which means that putting `plugin-files` in `nix.conf` causes random `nix` execution failures.
32+
Prefer instead to wrap the `nix` command using either the `NIX_CONFIG` environment variable or `--option plugin-files`.
33+
34+
Plugins should not be linked to any Lix libs directly, as those will be available already at load time (FIXME: is it an actual problem if they are, assuming that there are not version mismatches?).

tests/functional/plugins/plugintest.cc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ struct MySettings : Config
1616
"Whether the plugin-defined setting was set"};
1717
};
1818

19+
bool entryCalled = false;
20+
1921
MySettings mySettings;
2022

2123
static GlobalConfig::Register rs(&mySettings);
@@ -27,6 +29,7 @@ static void maybeRequireMeowForDlopen() {
2729

2830
static void prim_anotherNull (EvalState & state, const PosIdx pos, Value ** args, Value & v)
2931
{
32+
assert(entryCalled);
3033
if (mySettings.settingSet)
3134
v.mkNull();
3235
else
@@ -38,3 +41,8 @@ static RegisterPrimOp rp({
3841
.arity = 0,
3942
.fun = prim_anotherNull,
4043
});
44+
45+
extern "C" void nix_plugin_entry()
46+
{
47+
entryCalled = true;
48+
}

0 commit comments

Comments
 (0)