What stays out
Turned down on evidence, and each for a reason about the format rather than
about the plugin. All remain callable through raw, and any of them can be
declared in five lines of your own .toml — and an argument a .toml cannot
describe can often be handed to a raw argument instead, keeping the rest of
the declaration.
| Plugin | Scripts | Why |
|---|---|---|
Registry |
40 | Every scanned use is ${registry::…}, the Registry.nsh macro form, which needs a trailing ${registry::Unload} — behaviour, not arity. readReg, writeReg and deleteRegKey already cover 38 of the 40. |
nsJSON |
3 | Its node path is a variable number of positional strings — one to four across the scanned scripts — and params is a fixed list. Below. |
Inetc.post |
2 | Its body is popped before the flag loop (inetc.cpp:1369), so it has to be written ahead of every switch. A params entry is emitted after the flags, and there is no way to write one that comes first. |
SimpleSC.getErrorMessage |
61 | Takes its argument by Push — above. |
Nsis7z.extractWithCallback |
5 | Its second argument is a function address, and no params type describes one — below. |
LockedList |
0 | It is a custom page, not a call. Declaring only the Add* setup calls would ship half a feature. |
nsisunz.unzipToStack |
2 | Pushes one value per file in the archive, so the arity is 1 + n for an n nobody knows at compile time. Below. |
Crypto |
0 | No scanned script uses it. |
nsisunz’s other two methods are declarable and stay undeclared anyway: the
plugin is the worked example of one you declare yourself, in
Declaring a third-party plugin or header and in
tests/declarations.rs. Below.
nsisunz is the worked example, and its third method could not have shipped
Section titled “nsisunz is the worked example, and its third method could not have shipped”ZIP extraction, used in 21 scanned scripts. Source:
nsisunz.cpp, in the plugin’s own distribution.
Three exports over one function, and the count is what decides which:
void Unzip(…) { internal_unzip(0); }void UnzipToLog(…) { internal_unzip(1); }void UnzipToStack(…) { internal_unzip(2); }| Method | Scanned files | Arguments | Returns |
|---|---|---|---|
.unzip(zip, dest) |
1 | path, path |
string |
.unzipToLog(zip, dest) |
18 | path, path |
string |
.unzipToStack(zip, dest) |
2 | path, path |
1 + n |
The first two differ only in whether each extracted name is written to the
details log, and both push exactly one value: "success" (szSuccess,
nsisunz.cpp:27) or one of eight error sentences — "Error opening ZIP file",
"File not found in archive", "aborted" and five more. A sentence rather than
a code, so the comparison is ~= "success" and the value is worth printing as
it stands, exactly as with Inetc.
unzipToStack is the one that cannot be declared. Mode 2 pushes the names
as it walks the archive (nsisunz.cpp:418):
} else if (uselog == 2) { if (!first) { pushstring(""); // push list terminator (empty string) first++; } pushstring(pfn);}An empty string first, then one push per file, then the status last — so the
Lua arity is 1 + n for an n that is a property of the archive, not of the
program. That is neither a number nor a tagged/more fork: tagged says
which of two fixed lists of values, and this has no fixed list. It is
nsJSON’s problem on the output
side, and a .toml can describe neither.
The flags are otherwise a textbook case — a leading loop, order-free, one
bare switch and two valued ones (nsisunz.cpp:300):
popstring(buf);while (buf[0] == '/') { if (!lstrcmpi(buf+1, "text")) popstring(g_extract_text), hastext++; if (!lstrcmpi(buf+1, "noextractpath")) noextractpath++; if (!lstrcmpi(buf+1, "file")) popstring(filetoextract), usefile++; if (popstring(buf)) *buf = 0;}So unzipToLog would declare in a dozen lines and work. It stays out so that
this page keeps one complete example of a plugin left out, which is a decision about the documentation rather
than about the plugin — and the only entry in this file that is.
Why unzipToLog and not unzip. The example named unzip for two
releases, and a per-method count was never run: unzip appears in one of
the 984 scripts, unzipToLog in eighteen. A worked example is a page a reader
copies whole, so the method it names is the method they get, and pointing it at
the rare one taught the format correctly and the plugin wrongly.
nsJSON’s blocker is its path, not its flags
Section titled “nsJSON’s blocker is its path, not its flags”The recorded reason was the repeated, ordered flag run its readme advertises:
nsJSON::Get /index 0 /index 1 /index 3 /index 0 /endA table has unique keys and no order, so that form has no encoding
here. But no scanned script writes it — 25 call sites across three files, and
the repeated-/index form appears in none of them. It is a documentation
example, not a usage.
What the sites do write is a node path of positional strings, and its length is the problem:
nsJSON::Get "tag_name" /endnsJSON::Get /tree Manifest "builds" "windows" "${ARCH}" "url" /endnsJSON::Set /tree metrics "Data" "products" "Wii U USB Helper" /value `"$v"` /endOne segment to four, chosen per call. params is a fixed list, so a declaration
would have to pick a depth and be wrong at every other one — and being wrong
here is not a type error, it is a Pop count.
Two of the 25 also interleave — nsJSON::Get "assets" /index 0 "size" /end
puts a flag between two positionals, in an order the caller chose. Flags cannot
express that: it is a path query written in flag syntax, not
a flag list.
Two further facts for whoever revisits this. /tree, /value, /file and
/http are ordinary valued flags, but /value and /file come after the
path rather than before it — so the leading-only rule does not describe this
plugin either. And /end is mandatory for the reason it is mandatory on
Inetc: the readme says outright that it “must be added to the end of the list
to prevent stack corruption”, and this compiler’s caller-saves are exactly the
stack it would corrupt.
A way to write repeated flags would not unlock this plugin. A variadic params tail
would unlock 23 of the 25 sites, and is the thing to design if nsJSON is ever
wanted.
Until then it is raw — and this is the plugin the raw argument form was
built for. Declare get with one string parameter in your own .toml and
pass the whole path through it:
local node = nsJSON.get(raw "/index 0 /index 1 /index 3", "$Doc")The path becomes text nothing checks, which is the part a variadic params
would fix. What it buys in the meantime is the rest of the declaration: the call
is still one line the compiler writes, outputs still says how many Pops
follow, and local node still binds one — none of which survives writing the
same call as a raw block.
Nsis7z.extractWithCallback takes an address, not a callback
Section titled “Nsis7z.extractWithCallback takes an address, not a callback”The obvious reading is that it is an eighth member of the family
src/lower/callback.rs covers, turned down for the reason the six
FileFunc/TextFunc macros give: NSIS hands a callback its arguments in
registers it names, that map is behaviour, and behaviour stays out of a .toml.
nsis7z.cpp does not do that. Its handler pushes both values on the stack
and runs the code segment:
pushint((int)totalSize);pushint((int)completedSize);g_pluginExtra->ExecuteCodeSegment(progressCallback-1, 0);So the body pops completed first and total second — which is what the plugin’s own example does, and the only place that order is written down — and pushes nothing back. There is no sentinel, so no way to cancel the extraction, and no register protocol to get wrong.
The reason it stays out is a step earlier than the protocol. The plugin does not take a function; it takes the address of one, which the script obtains separately:
GetFunctionAddress $R9 CallbackTestNsis7z::ExtractWithCallback "Test.7z" $R9params has no type for an address, and GetFunctionAddress has no Lua
equivalent — it is a todo row that the compiler emits only for the nsDialogs
event handlers it generates itself, where the function’s address exists in
exactly one place. Declaring this method would mean giving a plugin argument the
address of a user-written function, which is exactly what that row exists to
prevent.
Worth recording for whoever revisits it: the body must pop exactly two, and the plugin fires the callback once per progress tick. A body that pops one leaves an int on the stack on every tick of every extraction.
/NOUNLOAD is not a flag
Section titled “/NOUNLOAD is not a flag”It is the most common flag in the scanned scripts — 99 sites, two and a half times
the next one — and it is not in any flags list here, nor can a project put it
in one.
It was never a plugin’s option. Source/script.cpp:5151 reads it off the front
of any plugin call, before the method’s own arguments, and passes it to
EW_REGISTERDLL as the bit that decides whether the DLL is freed after the
call. So it describes NSIS’s loader, not the method’s arguments, and a flags entry
naming it would claim the plugin parses a token the plugin never sees.
It is also deprecated, and has been since 2.42 (December 2008): “Deprecated
/NOUNLOAD and SetPluginsUnload to make scripts simpler and safer”. The plugin
API that replaced it lets a DLL that must stay resident say so itself. makensis
still accepts the token, and warns only when it is written in the wrong place
(DW_PLUGIN_NOUNLOAD_PLACEMENT) — the case where a plugin has a /NOUNLOAD
parameter of its own is the one that warning exists to catch.
Most of those 99 sites are habit from before 2.42 and need nothing. The
exception is an old plugin that leaves a thread or a window running inside its
DLL and never asks to stay loaded, like nxs. Freeing that DLL crashes the
installer some time later. For those, the declaration says
nounload = true
and every call writes the token, in first place. A call site still can’t write
it. nsProcess._Unload
stays undeclared: it releases a DLL kept loaded by /NOUNLOAD, and nsProcess
isn’t declared with it.