Skip to content

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 /end

A 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" /end
nsJSON::Get /tree Manifest "builds" "windows" "${ARCH}" "url" /end
nsJSON::Set /tree metrics "Data" "products" "Wii U USB Helper" /value `"$v"` /end

One 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 CallbackTest
Nsis7z::ExtractWithCallback "Test.7z" $R9

params 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.

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.