BP-PLUGIN, the plugin mechanism¶
Status: partial
Applies to: GCC 16.2.0 (tag releases/gcc-16.2.0)
Target-dependent: no
Generated sections: 2
Last verified: 2026-09-05 against releases/gcc-16.2.0
Sections 1, 2, 4, 6, 7 and 9 are written. Section 3 covers loading, registration, dispatch and pass insertion, and stops short of the front end events, which fire from four places each and want a document that knows what a declaration is. Section 5 has the command line surface and the diagnostics and no corpus entry behind it yet, which is marked where it applies. Section 8 lists the DejaGnu suite and does not yet say which test proves which invariant.
1. Purpose and scope¶
The plugin mechanism loads a shared object into the compiler proper and lets it register functions to be called at named points during a compilation. It is the only supported way to observe or change GCC's behaviour from outside without patching GCC.
What this document covers. The command line options that name a plugin and its arguments, the loading and initialization sequence, the version check that decides whether a plugin is allowed to run, the event table and how a callback is registered and dispatched, and the pseudo-event that inserts a pass into the pipeline.
What it does not cover. What any particular plugin does. The passes a plugin might register, which are BP-PIPELINE. The linker plugin, which is a different mechanism with a different header, include/plugin-api.h, and which is loaded by the linker rather than by GCC, and which people confuse with this one constantly because both are called a plugin.
Position in the pipeline. Not a stage. The mechanism spans the whole run of cc1. Options are collected during option handling, initialize_plugins at gcc/plugin.cc:772@releases/gcc-16.2.0 runs from toplev.cc before the front end starts, callbacks fire throughout, and finalize_plugins runs after the last event.
Inputs and outputs as properties. None. The mechanism itself neither requires nor provides nor destroys an IR property. A pass a plugin registers has properties like any other pass, and they are that pass's, not the mechanism's.
2. Data structures¶
2.1 The events¶
Generated by bpc build from the pinned GCC tree. Do not edit inside the markers, edit the generator.
gcc/plugin.def defines 26 events. 3 of them are pseudo-events, handled by register_callback at registration time and never fired. The remaining 23 are fired from 29 call sites. The number in the first column is the enumerator's value, and it is the index into plugin_callbacks and plugin_event_name, so the order of plugin.def is the ABI.
| # | Event | Data passed | Fired from |
|---|---|---|---|
| 0 | PLUGIN_START_PARSE_FUNCTION |
decl1 |
gcc/c/c-decl.cc:10781, gcc/cp/decl.cc:20170 |
| 1 | PLUGIN_FINISH_PARSE_FUNCTION |
current_function_decl / fndecl |
gcc/c/c-decl.cc:11696, gcc/cp/decl.cc:20841 |
| 2 | PLUGIN_PASS_MANAGER_SETUP |
struct register_pass_info *, at registration |
not fired |
| 3 | PLUGIN_FINISH_TYPE |
t.spec / type_spec |
gcc/c/c-parser.cc:3896, gcc/c/c-parser.cc:3906, gcc/cp/parser.cc:22403 |
| 4 | PLUGIN_FINISH_DECL |
decl |
gcc/c/c-decl.cc:6389, gcc/cp/decl.cc:10302 |
| 5 | PLUGIN_FINISH_UNIT |
none | gcc/toplev.cc:586 |
| 6 | PLUGIN_PRE_GENERICIZE |
fndecl |
gcc/c/c-decl.cc:11662, gcc/cp/decl.cc:20730 |
| 7 | PLUGIN_FINISH |
none | gcc/toplev.cc:2405 |
| 8 | PLUGIN_INFO |
struct plugin_info *, at registration |
not fired |
| 9 | PLUGIN_GGC_START |
none | gcc/ggc-page.cc:2328 |
| 10 | PLUGIN_GGC_MARKING |
none | gcc/ggc-common.cc:118 |
| 11 | PLUGIN_GGC_END |
none | gcc/ggc-page.cc:2345 |
| 12 | PLUGIN_REGISTER_GGC_ROOTS |
const struct ggc_root_tab *, at registration |
not fired |
| 13 | PLUGIN_ATTRIBUTES |
none | gcc/attribs.cc:338 |
| 14 | PLUGIN_START_UNIT |
none | gcc/toplev.cc:2221 |
| 15 | PLUGIN_PRAGMAS |
none | gcc/c-family/c-pragma.cc:1885 |
| 16 | PLUGIN_ALL_PASSES_START |
none | gcc/cgraphunit.cc:1872 |
| 17 | PLUGIN_ALL_PASSES_END |
none | gcc/cgraphunit.cc:1877 |
| 18 | PLUGIN_ALL_IPA_PASSES_START |
none | gcc/cgraphunit.cc:2240 |
| 19 | PLUGIN_ALL_IPA_PASSES_END |
none | gcc/cgraphunit.cc:2303 |
| 20 | PLUGIN_OVERRIDE_GATE |
gate_status, by address |
gcc/passes.cc:2605 |
| 21 | PLUGIN_PASS_EXECUTION |
pass |
gcc/passes.cc:2631 |
| 22 | PLUGIN_EARLY_GIMPLE_PASSES_START |
none | gcc/passes.cc:3122 |
| 23 | PLUGIN_EARLY_GIMPLE_PASSES_END |
none | gcc/passes.cc:3126 |
| 24 | PLUGIN_NEW_PASS |
new_pass |
gcc/passes.cc:1342 |
| 25 | PLUGIN_INCLUDE_FILE |
const_cast<char*> (ORDINARY_MAP_FILE_NAME (new_map)) |
gcc/c-family/c-opts.cc:1872 |
A row saying the data is none is a row where the callback is handed a null pointer. Nothing in the signature says so and nothing checks it.
The last two columns of that table are not written down anywhere in GCC. plugin.def says an event exists, the comment above it says what it is for in one clause, and where it fires and what pointer arrives with it live in twenty nine call sites spread across eleven files in three directories. A plugin that registers for PLUGIN_FINISH_TYPE and casts the data to the wrong thing gets a segfault in somebody else's compiler, so the type in the third column is the part of this document with the shortest path to a crash.
Two of the twenty three fired events are worth reading twice. PLUGIN_OVERRIDE_GATE is the only one whose data is written rather than read: it is handed the address of a bool and a callback that assigns through it changes whether the pass runs. PLUGIN_PASS_EXECUTION is the only one that fires once per pass per function, so it is the one that costs something, and section 5 has the number.
2.2 What GCC says each event is for¶
Generated by bpc build from the pinned GCC tree. Do not edit inside the markers, edit the generator.
| Event | What plugin.def says |
|---|---|
PLUGIN_START_PARSE_FUNCTION |
Called before parsing the body of a function. |
PLUGIN_FINISH_PARSE_FUNCTION |
After finishing parsing a function. |
PLUGIN_PASS_MANAGER_SETUP |
To hook into pass manager. |
PLUGIN_FINISH_TYPE |
After finishing parsing a type. |
PLUGIN_FINISH_DECL |
After finishing parsing a declaration. |
PLUGIN_FINISH_UNIT |
Useful for summary processing. |
PLUGIN_PRE_GENERICIZE |
Allows to see low level AST in C and C++ frontends. |
PLUGIN_FINISH |
Called before GCC exits. |
PLUGIN_INFO |
Information about the plugin. |
PLUGIN_GGC_START |
Called at start of GCC Garbage Collection. |
PLUGIN_GGC_MARKING |
Extend the GGC marking. |
PLUGIN_GGC_END |
Called at end of GGC. |
PLUGIN_REGISTER_GGC_ROOTS |
Register an extra GGC root table. |
PLUGIN_ATTRIBUTES |
Called during attribute registration. |
PLUGIN_START_UNIT |
Called before processing a translation unit. |
PLUGIN_PRAGMAS |
Called during pragma registration. |
PLUGIN_ALL_PASSES_START |
Called before first pass from all_passes. |
PLUGIN_ALL_PASSES_END |
Called after last pass from all_passes. |
PLUGIN_ALL_IPA_PASSES_START |
Called before first ipa pass. |
PLUGIN_ALL_IPA_PASSES_END |
Called after last ipa pass. |
PLUGIN_OVERRIDE_GATE |
Allows to override pass gate decision for current_pass. |
PLUGIN_PASS_EXECUTION |
Called before executing a pass. |
PLUGIN_EARLY_GIMPLE_PASSES_START |
Called before executing subpasses of a GIMPLE_PASS in execute_ipa_pass_list. |
PLUGIN_EARLY_GIMPLE_PASSES_END |
Called after executing subpasses of a GIMPLE_PASS in execute_ipa_pass_list. |
PLUGIN_NEW_PASS |
Called when a pass is first instantiated. |
PLUGIN_INCLUDE_FILE |
Called when a file is #include-d or given via the #line directive. this could happen many times. The event data is the included file path, as a const char* pointer. |
This is the entire specification of what these events mean. Everything else in this document about when an event fires was read out of the call site, because the call site is the only other place the answer exists.
2.3 The structures a plugin sees¶
Four, all declared extern "C" in gcc/plugin.h so that a plugin built by a different C++ compiler still links.
record PluginArgument gcc/plugin.h:42@releases/gcc-16.2.0
key string the part after the plugin name
value string or nothing nothing when the option had no =
record PluginInfo gcc/plugin.h:50@releases/gcc-16.2.0
version string printed by --version
help string printed by --help
record PluginGccVersion gcc/plugin.h:58@releases/gcc-16.2.0
basever string "16.2.0"
datestamp string "20260801"
devphase string "release", or "experimental"
revision string usually empty in a release
configuration_arguments string the full ./configure line
record PluginNameArgs gcc/plugin.h:68@releases/gcc-16.2.0
base_name string the filename with its extension removed
full_name string the path as -fplugin= gave it
argc integer how many -fplugin-arg- options matched
argv sequence of PluginArgument
version string or nothing filled in by PLUGIN_INFO
help string or nothing filled in by PLUGIN_INFO
base_name is the field with a consequence. It is computed from the file's own name by get_plugin_base_name at gcc/plugin.cc:160@releases/gcc-16.2.0, and it is what a -fplugin-arg- option has to spell. A plugin does not get to choose the prefix its own options are written with. Renaming the shared object renames the options.
2.4 The callback list¶
record CallbackInfo gcc/plugin.cc:104@releases/gcc-16.2.0
plugin_name string
func function (gcc_data, user_data) -> nothing
user_data pointer
next CallbackInfo or nothing
The lists live in plugin_callbacks, one head per event, indexed by the enumerator value from section 2.1. The array starts as a fixed one of PLUGIN_EVENT_FIRST_DYNAMIC entries at gcc/plugin.cc:113@releases/gcc-16.2.0 and is copied to the heap and grown the first time a plugin allocates an event of its own.
Registration pushes onto the head of the list at gcc/plugin.cc:514@releases/gcc-16.2.0. Dispatch walks from the head. So callbacks run in the reverse of the order they were registered in, both within one plugin and across plugins, and nothing says so anywhere.
2.5 The two symbols a plugin must export¶
plugin_is_GPL_compatible, an int, declared extern "C" at gcc/plugin.h:153@releases/gcc-16.2.0. Only its presence is looked at. It is never read, and a plugin that defines it and sets it to zero loads exactly like one that sets it to one.
plugin_init, with the signature at gcc/plugin.h:95@releases/gcc-16.2.0, returning zero for success.
3. Algorithms¶
3.1 Naming a plugin on the command line¶
-fplugin= and -fplugin-arg- are both Defer options at gcc/common.opt:2613@releases/gcc-16.2.0, meaning they are collected during parsing and replayed afterwards in command line order by handle_common_deferred_options at gcc/opts-global.cc:432@releases/gcc-16.2.0.
function add_new_plugin (name: string, table: Map)
complexity: O(1)
if name has no dot and no directory separator
name = plugin_directory() + "/" + name + platform_extension()
if name is not readable
fatal_error "inaccessible plugin file"
else
base = filename of name, with an extension of up to three characters removed
if base in table
if table[base].full_name != name
error "plugin was specified with different paths"
return
table[base] = PluginNameArgs(base_name: base, full_name: name, argc: 0, argv: [])
platform_extension() is .dll on MinGW, .dylib on Darwin and .so everywhere else, chosen at gcc/plugin.cc:204@releases/gcc-16.2.0. plugin_directory() is default_plugin_dir_name at gcc/plugin.cc:1043@releases/gcc-16.2.0, which returns what the driver passed as -iplugindir and fails hard if nothing did.
The extension removal is strip_off_ending at gcc/opts.cc:301@releases/gcc-16.2.0, and three characters is its real limit rather than a simplification. It looks for a dot at exactly three positions from the end, so .so, .dll and .cpp are removed and .dylib is not. Section 6 has what that costs.
function parse_plugin_arg_opt (arg: string, table: Map)
complexity: O(k) in the number of arguments this plugin already has
split arg at the first "-" into name and rest
split rest at the first "=" into key and value, value is nothing if there was no "="
if key is empty
error "malformed option -fplugin-arg-"
return
if name not in table
error "plugin should be specified before -fplugin-arg-"
return
append PluginArgument(key, value) to table[name].argv
The first hyphen separates the name from the key and every later one belongs to the key, so -fplugin-arg-foo-bar-baz=1 gives the plugin foo a key of bar-baz. The append reallocates the whole array each time at gcc/plugin.cc:338@releases/gcc-16.2.0, which is quadratic in the argument count and does not matter because the argument count is three.
3.2 Loading¶
function initialize_plugins (table: Map)
complexity: O(p) in the number of plugins
if table is empty
return
for each plugin in table in hash order
if not try_init_one_plugin(plugin)
remove plugin from table
for each ... in hash order is not a licence taken by the pseudocode. The traversal is htab_traverse_noresize at gcc/plugin.cc:782@releases/gcc-16.2.0 over a table hashed on base_name, so with two plugins on the command line the one that is initialized first is decided by the hash of its filename. Section 4 states this as an invariant because it is the sort of thing that works for a year and then does not.
function try_init_one_plugin (plugin: PluginNameArgs) -> true or false
complexity: O(1) plus whatever plugin_init does
handle = dlopen(plugin.full_name, RTLD_NOW | RTLD_GLOBAL)
if handle is nothing
error "cannot load plugin"
return false
if dlsym(handle, "plugin_is_GPL_compatible") is nothing
fatal_error "plugin is not licensed under a GPL-compatible license"
init = dlsym(handle, "plugin_init")
if init is nothing
dlclose(handle)
error "cannot find plugin_init in plugin"
return false
if init(plugin, address of gcc_version) != 0
dlclose(handle)
error "failed to initialize plugin"
return false
return true
RTLD_NOW is the load bearing flag. It resolves every undefined symbol at load time, so a plugin built against a header that no longer matches the compiler fails at dlopen with a symbol name rather than at the first call with a jump into nothing. RTLD_GLOBAL is there so that a plugin can itself dlopen something.
The handle is never closed on success, on purpose, and the comment at gcc/plugin.cc:738@releases/gcc-16.2.0 says why: the plugin has to stay mapped for the whole run, and there is nowhere later that would know to keep it.
The GPL check is fatal_error and everything else is error. A plugin without the symbol stops the compiler; a plugin that fails to initialize is removed from the table and the compilation continues to the point where the error count is looked at.
3.3 Registering a callback¶
function register_callback (name: string, event: integer, callback, user_data)
complexity: O(1)
if event == PLUGIN_PASS_MANAGER_SETUP
assert callback is nothing
register_pass(user_data)
return
if event == PLUGIN_INFO
assert callback is nothing
record user_data as this plugin's version and help
return
if event == PLUGIN_REGISTER_GGC_ROOTS
assert callback is nothing
add user_data to the GGC root tables
return
if event < 0 or event >= event_last
error "unknown callback event registered by plugin"
return
if callback is nothing
error "plugin registered a null callback function for event"
return
push CallbackInfo(name, callback, user_data) onto plugin_callbacks[event]
The three pseudo-events do their work now and store nothing. Passing a callback with one of them is gcc_assert (!callback) at gcc/plugin.cc:459@releases/gcc-16.2.0, which is an internal compiler error in a checking build and undefined behaviour in a release build, rather than a diagnostic.
A plugin can also allocate an event of its own by name, through get_named_event_id at gcc/plugin.cc:391@releases/gcc-16.2.0, which returns the id of an existing event or appends a new one and grows both arrays. That is how one plugin publishes an event for another plugin to hook, and it is why event_last is a variable rather than PLUGIN_EVENT_FIRST_DYNAMIC.
3.4 Dispatch¶
function invoke_plugin_callbacks (event: integer, gcc_data: pointer) -> integer
complexity: O(c) in the number of callbacks on this event
if no plugin was ever added
return PLUGEVT_NO_CALLBACK
start timer TV_PLUGIN_RUN
if event is one of the three pseudo-events
assert false
callbacks = plugin_callbacks[event]
if callbacks is empty
result = PLUGEVT_NO_CALLBACK
for each c in callbacks, head first
c.func(gcc_data, c.user_data)
stop timer TV_PLUGIN_RUN
return result
The first check is flag_plugin_added at gcc/plugin.h:188@releases/gcc-16.2.0, tested in an inline wrapper in the header so that the twenty nine call sites cost one load and a branch each in a compiler with no plugins loaded. The out of line half at gcc/plugin.cc:546@releases/gcc-16.2.0 is only reached once a plugin exists, which is why the timer is inside it and a plugin free compilation has no plugin timer at all.
There is no way to stop the walk. Every callback on an event runs, the return value of a callback is void, and the return value of the dispatch says only whether there was at least one.
3.5 Registering a pass¶
PLUGIN_PASS_MANAGER_SETUP hands register_callback a register_pass_info at gcc/tree-pass.h:335@releases/gcc-16.2.0, which is a pass, the name of an existing pass to position against, an instance number, and one of PASS_POS_INSERT_AFTER, PASS_POS_INSERT_BEFORE or PASS_POS_REPLACE at gcc/tree-pass.h:328@releases/gcc-16.2.0.
function register_pass (info: RegisterPassInfo)
complexity: O(n) in the number of passes, five times over
if info.pass is nothing
fatal_error "plugin cannot register a missing pass"
if info.pass.name is nothing
fatal_error "plugin cannot register an unnamed pass"
if info.reference_pass_name is nothing
fatal_error "plugin cannot register pass without reference pass name"
every = info.ref_pass_instance_number == 0
placed = false
for each list in [lowering, small_ipa, regular_ipa, late_ipa, all]
if not placed or every
placed = placed or position_pass(info, list)
if not placed
fatal_error "pass not found but is referenced by new pass"
register dump files for the new pass and each of its duplicates
An instance number of zero means every instance of the reference pass, which is why the loop keeps going after a hit in that case: dom runs three times and a plugin that positions against it with zero gets three copies of its pass. All five lists are searched because a plugin has no way to know which one holds the pass it named.
Every check here is fatal_error rather than error, and the messages say "plugin" out loud because, as the comment at gcc/passes.cc:1503@releases/gcc-16.2.0 puts it, GCC's own passes never fail them.
4. Invariants¶
I1. The order of DEFEVENT lines in gcc/plugin.def is the numbering of enum plugin_event, and that numbering is the index into both plugin_callbacks and plugin_event_name.
Established by: the #include "plugin.def" in the enum at gcc/plugin.h:26@releases/gcc-16.2.0 and in the name array at gcc/plugin.cc:55@releases/gcc-16.2.0. Checked by: nothing. May be broken by: nobody, and a plugin built before a reordering would call the wrong callback rather than fail, which is why section 9 calls this the real ABI.
I2. A plugin is only ever loaded into a compiler for which plugin_default_version_check would return true, unless the plugin decided otherwise.
Established by: the plugin, in its own plugin_init. Checked by: nothing in GCC. May be broken by: any plugin, at any time, by not calling the check. The compiler offers the comparison at gcc/plugin.cc:1007@releases/gcc-16.2.0 and does not perform it.
I3. plugin_callbacks[e] is a list of callbacks in the reverse of their registration order, for every real event e.
Established by: the head insertion at gcc/plugin.cc:514@releases/gcc-16.2.0. Checked by: nothing. May be broken by: unregister_callback, which removes the first entry belonging to a named plugin and leaves the rest in order.
I4. No callback is ever stored for PLUGIN_PASS_MANAGER_SETUP, PLUGIN_INFO or PLUGIN_REGISTER_GGC_ROOTS.
Established by: the three early returns in register_callback at gcc/plugin.cc:458@releases/gcc-16.2.0. Checked by: gcc_assert (false) in invoke_plugin_callbacks_full at gcc/plugin.cc:596@releases/gcc-16.2.0, which fires if one is ever dispatched. May be broken by: nobody.
I5. The order in which plugins are initialized is the hash order of their base names, not the order they appear on the command line.
Established by: htab_traverse_noresize at gcc/plugin.cc:782@releases/gcc-16.2.0. Checked by: nothing. May be broken by: nobody, and two plugins that both register for the same event and care which runs first have no supported way to say so.
I6. Once initialize_plugins returns, the set of loaded plugins does not change for the rest of the run.
Established by: initialize_plugins being called exactly once, from gcc/toplev.cc:2361@releases/gcc-16.2.0. Checked by: nothing. May be broken by: nobody.
I7. Every dynamically allocated event id is at least PLUGIN_EVENT_FIRST_DYNAMIC and less than event_last.
Established by: get_named_event_id at gcc/plugin.cc:391@releases/gcc-16.2.0. Checked by: the range test in register_callback at gcc/plugin.cc:472@releases/gcc-16.2.0, which reports an error, and by gcc_assert in the dispatcher at gcc/plugin.cc:556@releases/gcc-16.2.0. May be broken by: nobody.
I8. A callback registered for an event whose data column in section 2.1 reads none is called with a null gcc_data.
Established by: the call sites. Checked by: nothing. May be broken by: nobody, and the signature gives a plugin no way to find out other than reading the table.
I9. PLUGIN_PASS_EXECUTION fires only for a pass that is about to run, after its gate returned true and after should_skip_pass_p declined to skip it, and before the pass's dump file is opened and its timer started.
Established by: the placement at gcc/passes.cc:2631@releases/gcc-16.2.0, thirty two lines after the gate test. Checked by: nothing. May be broken by: nobody. This is the invariant that makes the event usable for timing, and it is also why the count of events is smaller than the count of passes in the pipeline.
5. Observable behaviour¶
gcc -print-file-name=plugin prints the directory the plugin headers and the default plugin location live in. This is the same path the driver passes to cc1 as -iplugindir, injected by the find-plugindir spec function at gcc/gcc.cc:10823@releases/gcc-16.2.0, and it is how a build system finds gcc-plugin.h without guessing.
-fplugin=NAME with no dot and no slash is looked up in that directory with a platform extension appended. -fplugin=./x.so is a path and is used as written.
-fplugin-arg-BASE-KEY and -fplugin-arg-BASE-KEY=VALUE add to a plugin's argv, where BASE is the shared object's filename with its extension removed and not a name the plugin chose.
--help prints a plugin's help string, under the heading Help for the loaded plugins:, for each plugin that registered PLUGIN_INFO. --version prints its version string.
-fdump-passes lists a plugin's registered passes along with GCC's own, because initialize_plugins runs at gcc/toplev.cc:2361@releases/gcc-16.2.0 and handle_deferred_dump_options runs three lines later. A plugin pass therefore also gets a -fdump- switch of its own.
-ftime-report grows two rows, plugin initialization and plugin execution, from the timevars at gcc/timevar.def:312@releases/gcc-16.2.0. The second is the total across every callback on every event, so a plugin that hooks PLUGIN_PASS_EXECUTION sees its own cost attributed there rather than spread across the passes it watched.
An internal compiler error with a plugin loaded prints *** WARNING *** there are active plugins, do not report this as a bug unless you can reproduce it without enabling any plugins. at gcc/plugin.cc:996@releases/gcc-16.2.0, followed by a table of events and the plugins registered for each. warn_if_plugins is called from the ICE path at gcc/toplev.cc:1037@releases/gcc-16.2.0.
The one number this project has measured: a plugin registering for PLUGIN_PASS_EXECUTION and PLUGIN_ALL_PASSES_END sees 247 pass executions on a four line C function at -O2 on aarch64, against a pipeline of 395 passes. The difference is gates and skipped passes, which is I9 seen from outside.
No corpus entry records any of this yet. Everything above was read from the source or measured by hand, and until there is a recording it is a claim rather than an observation.
6. Edge cases and error paths¶
Plugin support disabled at configure time. -fplugin= is still accepted by the option parser and produces plugin support is disabled; configure with --enable-plugin at gcc/opts-global.cc:436@releases/gcc-16.2.0. Nothing else in the mechanism is compiled in, and invoke_plugin_callbacks becomes a function that returns a constant.
A short name with no -iplugindir. fatal_error at gcc/plugin.cc:1047@releases/gcc-16.2.0, saying the option was not passed from the gcc driver. This is what running cc1 by hand looks like.
A short name that does not resolve. fatal_error at gcc/plugin.cc:214@releases/gcc-16.2.0, which prints both the expanded path and the short name, because the expansion is the part that surprises people.
A .dylib extension. strip_off_ending at gcc/opts.cc:301@releases/gcc-16.2.0 removes a dot only if it is two, three or four characters from the end, so a five character extension survives. On Darwin, -fplugin=/path/to/gxplug.dylib produces a base_name of gxplug.dylib, and the plugin's options then have to be written -fplugin-arg-gxplug.dylib-key=value. Nothing warns. Building the plugin as .so on Darwin as well as on Linux avoids the whole thing, and is the reason to do it.
The same plugin twice. Identical paths are the second one being ignored silently. Different paths for the same base name are an error at gcc/plugin.cc:236@releases/gcc-16.2.0. Two genuinely different plugins whose files happen to share a base name are the second case, and the message talks about paths rather than about the collision.
-fplugin-arg- before -fplugin=. An error at gcc/plugin.cc:359@releases/gcc-16.2.0. The deferred options are replayed in command line order, so the argument arrives before the plugin exists in the table. The order of two unrelated looking options is significant and the only sign of it is this message.
-fplugin-arg-foo with no key. An error at gcc/plugin.cc:296@releases/gcc-16.2.0. A key with an empty value, written -fplugin-arg-foo-key=, is legal and gives an empty string rather than nothing.
Missing plugin_is_GPL_compatible. fatal_error at gcc/plugin.cc:716@releases/gcc-16.2.0. Note that the C++ name mangling of that symbol would make it undiscoverable, which is why gcc/plugin.h:153@releases/gcc-16.2.0 declares it extern "C" and a plugin that includes the header gets that for free.
Missing plugin_init, or plugin_init returning nonzero. An error, a dlclose, and the plugin is dropped from the table by init_one_plugin at gcc/plugin.cc:753@releases/gcc-16.2.0. The rest of the compilation proceeds, and since an error has been reported it will not produce output.
A version mismatch. Nothing happens, because nothing in GCC checks. plugin_default_version_check compares five strings and the fifth is configuration_arguments, the entire ./configure command line. Two GCC 16.2.0 builds of the same source with different --prefix values fail that comparison. A plugin that calls the check is therefore tied to one build of one compiler, and a plugin that does not call it will load into anything and crash when a structure layout has moved. There is no middle option in the API.
An unknown event id. An error from register_callback at gcc/plugin.cc:474@releases/gcc-16.2.0, which prints the plugin name and nothing about the id.
A null callback for a real event. An error at gcc/plugin.cc:506@releases/gcc-16.2.0 naming both the plugin and the event, using plugin_event_name, so this is one of the few diagnostics that spells an event out.
A callback for a pseudo-event. gcc_assert (!callback), so an internal compiler error in a checking build. There is no diagnostic.
A pass registered against a name that does not exist. fatal_error at gcc/passes.cc:1534@releases/gcc-16.2.0 naming both passes. A misspelled reference pass name is caught, and a correctly spelled one that is only in a list this GCC does not use is caught the same way.
A crash inside a plugin. Indistinguishable from a crash in GCC, other than by the warning banner in section 5. There is no isolation, no signal handling around a callback, and no way to disable one plugin after it has been loaded.
Reentrancy. PLUGIN_GGC_MARKING fires during garbage collection, so a callback on it may not allocate GC memory. Nothing enforces this. The other events are all called from ordinary compiler code and are reentrant in the sense that the compiler is.
7. Interactions¶
Option handling. gcc/common.opt:2613@releases/gcc-16.2.0 defers both options into common_deferred_options, and gcc/opts-global.cc:432@releases/gcc-16.2.0 replays them. The driver contributes -iplugindir through the spec at gcc/gcc.cc:1282@releases/gcc-16.2.0, only when a -fplugin was given and no -iplugindir was.
toplev.cc, in order. initialize_plugins at line 2361, handle_deferred_dump_options at 2363, print_plugins_help at 2371 when --help was given, PLUGIN_START_UNIT at 2221 after the front end is initialized, PLUGIN_FINISH_UNIT at 586 at the end of compile_file, PLUGIN_FINISH at 2405, finalize_plugins at 2417. All at releases/gcc-16.2.0.
The pass manager. register_pass at gcc/passes.cc:1499@releases/gcc-16.2.0 is the write side. PLUGIN_NEW_PASS at gcc/passes.cc:1342@releases/gcc-16.2.0 fires from add_pass_instance for the first instance of every pass, including GCC's own, so a plugin sees the whole pipeline being built. PLUGIN_OVERRIDE_GATE and PLUGIN_PASS_EXECUTION are both inside execute_one_pass at gcc/passes.cc:2579@releases/gcc-16.2.0.
The garbage collector. PLUGIN_GGC_START and PLUGIN_GGC_END bracket a collection in gcc/ggc-page.cc, and PLUGIN_GGC_MARKING fires from ggc_mark_roots at gcc/ggc-common.cc:118@releases/gcc-16.2.0. PLUGIN_REGISTER_GGC_ROOTS is how a plugin keeps its own GC pointers alive without hooking marking at all.
The front ends. C and C++ each fire PLUGIN_START_PARSE_FUNCTION, PLUGIN_FINISH_PARSE_FUNCTION, PLUGIN_FINISH_TYPE, PLUGIN_FINISH_DECL and PLUGIN_PRE_GENERICIZE, from the files in section 2.1. No other front end fires any of them, so a plugin written against these events works for two of GCC's languages and silently does nothing for the rest.
Attributes and pragmas. PLUGIN_ATTRIBUTES at gcc/attribs.cc:338@releases/gcc-16.2.0 and PLUGIN_PRAGMAS at gcc/c-family/c-pragma.cc:1885@releases/gcc-16.2.0 are the two events whose entire purpose is to give a callback the one moment when registering something is allowed.
Globals touched. plugin_name_args_tab, the table of plugins. plugin_callbacks and plugin_event_name, both of which may be reallocated by get_named_event_id. event_last and event_horizon. flag_plugin_added, read by the inline dispatcher. plugindir_string, written by the option parser. gcc_version, from plugin-version.h, passed to every plugin_init. Section 3 passes several of these as arguments and they are all globals.
Target hooks. None. The mechanism consults no target hook, which is why the header says it is not target dependent, and section 9 explains why that is less true than it sounds.
8. Conformance¶
The suite is gcc/testsuite/gcc.dg/plugin/, driven by plugin.exp, which returns immediately unless the DejaGnu global ENABLE_PLUGIN is set, so a compiler configured without plugin support reports zero tests rather than failures. plugin-support.exp builds each plugin from source with the compiler under test before running the tests that use it, which makes this the one part of the testsuite that exercises the plugin ABI rather than asserting it.
The tests that cover the mechanism itself rather than using it as a tool:
| Plugin source | Tests | What it proves |
|---|---|---|
one_time_plugin.cc |
one_time-test-1.c |
a pass registered with an instance number of 0 is inserted at every instance of the reference pass, and complains if it runs more than once |
start_unit_plugin.cc |
start_unit-test-1.c |
PLUGIN_START_UNIT fires, once |
finish_unit_plugin.cc |
finish_unit-test-1.c |
PLUGIN_FINISH_UNIT fires, once |
ggcplug.cc |
ggcplug-test-1.c |
the three GGC events fire and PLUGIN_REGISTER_GGC_ROOTS keeps a plugin's roots alive |
selfassign.cc |
self-assign-test-1.c, self-assign-test-2.c |
a registered GIMPLE pass sees the IR and can emit a diagnostic from it |
dump_plugin.cc |
dump-1.c, dump-2.c |
a plugin pass gets its own -fdump- switch |
Everything else under gcc.dg/plugin/, which is most of the 191 files, uses a plugin to reach a part of GCC that has no other test surface: the diagnostics machinery, the analyzer, wide_int, poly_int, and the crash handler. Those are conformance tests for other blueprints and they are load bearing for this one anyway, because every one of them fails if the plugin ABI is broken.
The invariants from section 4 restated as assertions:
- I1: the enumerator value of an event equals its zero based line index among
DEFEVENTlines inplugin.def. The generated table in section 2.1 is this assertion, andbpc checkruns it. - I4: registering a callback for a pseudo-event reaches
gcc_assert (!callback). No test exercises this. - I5: with two plugins on the command line, the order of their
plugin_initcalls is not the command line order for at least one pair of names. No test exercises this. - I9: the number of
PLUGIN_PASS_EXECUTIONevents is at most the number of passes in the pipeline, and is strictly less at any optimization level where some gate is closed. No test exercises this.
To be written: which existing test, if any, would fail for each of the remaining invariants.
9. Port notes¶
The header says this component is not target dependent, and that is true of the code and false of the experience. Nothing here consults a target hook. Everything here depends on the host's dynamic loader.
Three host variants, in one file. The POSIX try_init_one_plugin at gcc/plugin.cc:692@releases/gcc-16.2.0 uses dlopen and dlsym. A second one at gcc/plugin.cc:630@releases/gcc-16.2.0, compiled instead of it under __MINGW32__, uses LoadLibrary and GetProcAddress and formats its errors through FormatMessageA. The two produce different message text for the same failure. A host with no dynamic loading at all builds with ENABLE_PLUGIN off and the mechanism is absent.
What differs across hosts:
| ELF | Darwin | MinGW | |
|---|---|---|---|
| extension for a short name | .so |
.dylib |
.dll |
extension stripped for base_name |
yes | no, it is five characters | yes |
| undefined symbols in the plugin | resolved from the executable by default | need -undefined dynamic_lookup at link time |
resolved against an import library |
| loader | dlopen |
dlopen |
LoadLibrary |
What is forced. The two exported symbols, because dlsym is the only way in. The order of plugin.def, because it is compiled into both the plugin and the compiler as bare integers and nothing carries a name across the boundary at run time. RTLD_NOW, in the weaker sense that lazy binding would turn a build mismatch from a load failure into a crash much later.
What is not forced, and matters most. Putting configuration_arguments in the version comparison. The other four fields identify the source; the fifth identifies one particular build of it. Comparing it means a plugin is valid for exactly one compiler binary, which is a defensible answer to a real problem, and it is a choice rather than a consequence. GCC's own structure layouts depend on a handful of configure options and not on the whole line, and an implementation that compared a hash of the layout affecting options, or that exported a layout version, would let a plugin built once work with every compatible build. GCC does not do that, plugin_default_version_check is advisory anyway, and the result is that in practice a plugin is rebuilt for each compiler it runs in. This project does exactly that, and it is why the container image for plugin work builds the plugin rather than shipping one.
Also not forced. Reverse registration order for callbacks, which is head insertion into a singly linked list and nothing more. Hash order for plugin initialization, which is a traversal of a table built for lookup being used for iteration. Both are unspecified in the documentation, both are relied on by nobody on purpose, and both are the kind of thing that a reimplementation should either fix or write down.
The event table is the ABI. An independent implementation is free to choose a different set of events, and is not free to renumber this one. A plugin compiled against plugin.def holds the integers, not the names, and plugin_default_version_check does not look at the event table at all, so a compiler that inserted an event in the middle and kept the same version string would dispatch a plugin's callback to the wrong hook with the wrong data. Appending is safe. Inserting is not, and nothing in GCC would notice.