Languages: English | 简体中文 | 繁體中文 | 日本語 | 한국어 | Français | Deutsch | Español | Italiano | Русский | العربية
← Documentation index · ← NeverC project
A NeverC plugin is a shared module that exports exactly one function, negotiates versioned capability tables by 128-bit interface ID, and attaches itself to a frozen graph of named compiler phases. The whole interface is pure C11. A plugin never includes an LLVM header, never links the compiler, and never passes a C++ type across the boundary.
NEVERC_EXPORT NevercStatus NEVERC_CALL
neverc_plugin_entry(const NevercBootstrapAPI *Bootstrap,
NevercPluginDescriptor *OutPlugin);That signature, declared in PluginCore.h, is the entire linkage contract.
Everything else — reading IR, rewriting an object graph, replacing the
optimization pipeline — is reached through tables you ask the host for by ID.
| Guide | Covers |
|---|---|
| Driver API | Command line, toolchain selection, action graph, job graph |
| Source and I/O API | VFS providers, source locations, buffers, output sinks, dependencies |
| Preprocessor API | Tokens, macros, pragmas, includes, feature queries, 39 event kinds |
| AST and semantic API | Parser extension, AST mutation, name lookup, types, constants |
| IR API | LLVM IR reading, transactional building, analyses, passes, providers |
| MIR API | Machine functions, registers, frames, MIR passes and analyses |
| Target, MC, assembly, object | Target registration, calling conventions, MC encoding, object graphs |
| Link and LTO API | Link graph, symbol resolution, GC/ICF, linker and LTO providers |
| DynCode API | Flat position-independent images, import lowering, charset encoding |
| Custom calling conventions | Data-driven calling-convention plugins |
| Phase coverage evidence | Test mapping for every stable phase |
The host drives a plugin through three nested scopes. Each scope hands the plugin an opaque state pointer that the plugin allocates and owns, so a correctly written plugin needs no global mutable state.
| Scope | Callbacks | Meaning |
|---|---|---|
| Process | ProcessBegin, Register, Destroy |
One compiler process. Query interfaces and register capabilities here. |
| Session | SessionBegin, SessionEnd |
One driver invocation. |
| Task | TaskBegin, TaskEnd |
One unit of work, identified by NevercTaskKind. |
typedef struct NevercPluginDescriptor {
NevercABITableHeader Header;
NevercStringView PluginID;
NevercStringView DisplayName;
NevercSemanticVersion Version;
NevercConcurrencyModel Concurrency;
NevercReentrancyModel Reentrancy;
NevercStructArrayView RequiredInterfaces; /* NevercInterfaceRequirement[] */
NevercStructArrayView OptionalInterfaces; /* NevercInterfaceRequirement[] */
NevercStructArrayView Dependencies; /* NevercPluginDependency[] */
NevercProcessBeginFn ProcessBegin;
NevercRegisterPluginFn Register;
NevercSessionBeginFn SessionBegin;
NevercSessionEndFn SessionEnd;
NevercTaskBeginFn TaskBegin;
NevercTaskEndFn TaskEnd;
NevercPluginDestroyFn Destroy;
} NevercPluginDescriptor;Only PluginID and Register are practically mandatory; every callback slot
may stay NULL. Task kinds are NEVERC_TASK_INVOCATION,
TRANSLATION_UNIT, LTO, LINK, CODEGEN, OBJECT, and DYNCODE.
The host calls ProcessBegin first, then Register exactly once.
Registration is the only place where options, observers, interceptors, and
providers may be added; the phase graph is frozen afterwards.
State is retrieved inside a callback rather than captured:
Core->GetSessionState(Core->Context, Frame->Session, PluginID, &SessionState);
Core->GetTaskState(Core->Context, Frame->Task, PluginID, &TaskState);A phase is a named, versioned transition from an input artifact to an output artifact. NeverC ships 130 built-in phases, plus 8 extension-ID families reserved for plugin-defined phases:
| Domain | Phases | Domain | Phases |
|---|---|---|---|
driver |
6 | mir |
10 |
source |
3 | codegen |
4 |
prep |
6 | mc |
13 |
syntax |
7 | assembly |
4 |
sema |
7 | object |
8 |
ir |
8 | link |
20 |
dyncode |
34 |
Every one of the 130 is stability tier stable in ABI major 1. Each phase
advertises a policy, and a plugin may attach itself only in the ways that
policy permits:
| Policy flag | Phases | What a plugin may do |
|---|---|---|
NEVERC_PHASE_OBSERVABLE |
130 | Register an observer for read-only notification. |
NEVERC_PHASE_INTERCEPTABLE |
105 | Wrap the phase and decide whether to call the rest of the chain. |
NEVERC_PHASE_REPLACEABLE |
86 | Register a provider that supplies the output itself. |
NEVERC_PHASE_SKIPPABLE_WITH_PROOF |
13 | Skip the transition while supplying a proof handle. |
NEVERC_PHASE_SEALED_HOST_GATE |
14 | Nothing. Verifiers and commits are host-owned. |
The 14 sealed gates are ir.final_verify, mir.final_verify,
codegen.product_verify, assembly.final_verify, assembly.commit,
object.final_verify, object.commit, link.image_verify,
link.side_outputs_verify, link.commit, dyncode.ir.final_verify,
dyncode.mir.final_verify, dyncode.verify, and dyncode.commit. They can
be observed but never intercepted, replaced, or skipped.
Observers are delivered at the points a phase declares:
NEVERC_OBSERVER_BEFORE, NEVERC_OBSERVER_AFTER, and
NEVERC_OBSERVER_AFTER_COMMIT. An interceptor receives a
NevercPhaseContinuation and must call InvokeNext at most once, on the
callback thread, then report NEVERC_PHASE_CONTINUE, NEVERC_PHASE_REPLACE,
or NEVERC_PHASE_SKIP in NevercPhaseResult.Action.
Every phase callback receives the same frame:
typedef struct NevercPhaseFrame {
NevercABITableHeader Header;
NevercSessionHandle Session;
NevercTaskHandle Task;
NevercInterfaceID Phase;
NevercPhaseRoute Route; /* triple, CPU, features, object format */
NevercArtifactHandle Input;
NevercArtifactHandle CurrentOutput;
NevercHandle Cancellation;
} NevercPhaseFrame;Schema/PhaseSchema.json is the normative source for phase IDs, policies,
stability tiers, and verifier gates. The generated
Schema/PluginPhaseSchema.inc exposes each of them as compile-time
constants — for phase neverc.ir.pass.pipeline_start:
NEVERC_PHASE_IR_PASS_PIPELINE_START_NAME /* "neverc.ir.pass.pipeline_start" */
NEVERC_PHASE_IR_PASS_PIPELINE_START_HIGH /* UINT64_C(0x4e43504849520001) */
NEVERC_PHASE_IR_PASS_PIPELINE_START_LOW /* UINT64_C(0x0000000000000004) */
NEVERC_PHASE_IR_PASS_PIPELINE_START_POLICY /* OBSERVABLE | INTERCEPTABLE */
NEVERC_PHASE_IR_PASS_PIPELINE_START_STABILITY
NEVERC_PHASE_IR_PASS_PIPELINE_START_INPUT_HIGH /* and _INPUT_LOW, _OUTPUT_* */NEVERC_BUILTIN_PHASE_COUNT and the per-domain
NEVERC_BUILTIN_<DOMAIN>_PHASE_COUNT constants let a plugin assert the graph
it was compiled against.
This is pluginsdk/templates/minimal/Plugin.c verbatim. It loads, negotiates
the ABI, registers nothing, and unloads cleanly — copy the directory and grow
it from here.
#include "neverc/Plugin/NevercPluginAPI.h"
#define MINIMAL_PLUGIN_ID "com.example.minimal"
#define STRING_VIEW_LITERAL(Text) \
{ (Text), (uint64_t)(sizeof(Text) - 1) }
static NevercStatus status_code(NevercStatusCode Code) {
NevercStatus Status = neverc_status_ok();
Status.Code = Code;
return Status;
}
static void copy_bytes(void *Destination, const void *Source, uint64_t Count) {
uint64_t Index;
unsigned char *Out = (unsigned char *)Destination;
const unsigned char *In = (const unsigned char *)Source;
for (Index = 0; Index != Count; ++Index)
Out[Index] = In[Index];
}
static NevercStatus NEVERC_CALL
process_begin(const NevercCoreAPI *Core, void **OutProcessState) {
if (Core == NULL || OutProcessState == NULL)
return status_code(NEVERC_STATUS_INVALID_ARGUMENT);
*OutProcessState = NULL;
return neverc_status_ok();
}
static NevercStatus NEVERC_CALL
register_plugin(const NevercCoreAPI *Core, const NevercRegistrarAPI *Registrar,
void *RegistrarContext, void *ProcessState) {
(void)Core;
(void)RegistrarContext;
(void)ProcessState;
if (Registrar == NULL)
return status_code(NEVERC_STATUS_INVALID_ARGUMENT);
/* Register options, observers, interceptors, or providers here. */
return neverc_status_ok();
}
NEVERC_EXPORT NevercStatus NEVERC_CALL
neverc_plugin_entry(const NevercBootstrapAPI *Bootstrap,
NevercPluginDescriptor *OutPlugin) {
NevercPluginDescriptor Descriptor = {0};
uint32_t Capacity;
uint64_t BytesToWrite;
if (Bootstrap == NULL || OutPlugin == NULL ||
OutPlugin->Header.StructSize < sizeof(uint32_t))
return status_code(NEVERC_STATUS_INVALID_ARGUMENT);
Capacity = OutPlugin->Header.StructSize;
Descriptor.Header = (NevercABITableHeader){
sizeof(Descriptor), NEVERC_PLUGIN_ABI_MAJOR, NEVERC_PLUGIN_ABI_MINOR, 0};
Descriptor.PluginID = (NevercStringView)STRING_VIEW_LITERAL(MINIMAL_PLUGIN_ID);
Descriptor.DisplayName =
(NevercStringView)STRING_VIEW_LITERAL("Minimal Plugin");
Descriptor.Version = (NevercSemanticVersion){1, 0, 0, 0};
Descriptor.Concurrency = NEVERC_CONCURRENCY_SESSION_SERIAL;
Descriptor.Reentrancy = NEVERC_REENTRANCY_ALLOWED;
Descriptor.ProcessBegin = process_begin;
Descriptor.Register = register_plugin;
BytesToWrite = Capacity < sizeof(Descriptor) ? Capacity : sizeof(Descriptor);
copy_bytes(OutPlugin, &Descriptor, BytesToWrite);
return neverc_status_ok();
}OutPlugin is a caller-owned buffer. On entry its Header.StructSize is the
writable capacity; the plugin writes at most that many bytes and reports the
size it actually produced. Writing the descriptor's own Header first, then
truncating the copy, satisfies both halves of that rule.
Capability tables are fetched by 128-bit interface ID, not by symbol. Ask for the major version you compiled against and the lowest minor you can work with:
const void *Table = NULL;
uint16_t Minor = 0;
uint64_t TableSize = 0;
NevercStatus Status = Bootstrap->QueryInterface(
Bootstrap->Context,
(NevercInterfaceID){NEVERC_INTERFACE_IR_PASS_HIGH,
NEVERC_INTERFACE_IR_PASS_LOW},
NEVERC_IR_PASS_API_MAJOR, NEVERC_IR_PASS_API_MINOR, &Table, &Minor,
&TableSize);
if (Status.Code != NEVERC_STATUS_OK)
return Status;
if (!Table || TableSize < offsetof(NevercIRPassAPI, RegisterPass) +
sizeof(((NevercIRPassAPI *)0)->RegisterPass))
return fail(NEVERC_STATUS_ABI_MISMATCH);Checking TableSize against the offset of the last function you call is the
rule that makes the ABI extensible: a newer host appends fields, and an older
plugin keeps working because it never reads past the prefix it verified. The
NEVERC_ABI_FIELD_AVAILABLE(header, type, field) macro applies the same test
to a struct you received. The same QueryInterface signature is also on
NevercCoreAPI, so you can negotiate late instead of at entry.
The public interfaces, their tables, and their ID macros:
| Interface macro pair | Table | Header |
|---|---|---|
NEVERC_INTERFACE_CORE_{HIGH,LOW} |
NevercCoreAPI |
PluginCore.h |
NEVERC_INTERFACE_DRIVER_* |
NevercDriverAPI |
PluginDriver.h |
NEVERC_INTERFACE_IO_*, ..._SOURCE_LOCATION_* |
NevercIOAPI, NevercSourceLocationAPI |
PluginSource.h |
NEVERC_INTERFACE_PREP_* |
NevercPrepAPI |
PluginPrep.h |
NEVERC_INTERFACE_AST_*, ..._PARSER_* |
NevercASTAPI, NevercParserAPI |
PluginAST.h |
NEVERC_INTERFACE_SEMA_* |
NevercSemaAPI |
PluginSema.h |
NEVERC_INTERFACE_IR_CORE_*, ..._IR_BUILDER_*, ..._IR_ANALYSIS_*, ..._IR_PASS_*, ..._IR_GEN_*, ..._IR_OPTIMIZATION_* |
six IR tables | PluginIR.h |
NEVERC_INTERFACE_TARGET_*, ..._TARGET_ABI_*, ..._CALLING_CONVENTION_* |
NevercTargetAPI, NevercTargetABIAPI, NevercCallingConventionAPI |
PluginTarget.h |
NEVERC_INTERFACE_MIR_*, ..._MIR_ANALYSIS_*, ..._MIR_PASS_*, ..._MIR_PROVIDER_* |
four MIR tables | PluginMIR.h |
NEVERC_INTERFACE_MC_*, ..._MC_EMISSION_*, ..._MC_PROVIDER_*, ..._ASSEMBLY_PROVIDER_* |
four MC tables | PluginMC.h |
NEVERC_INTERFACE_OBJECT_*, ..._OBJECT_FORMAT_*, ..._OBJECT_PHASE_* |
three object tables | PluginObject.h |
NEVERC_INTERFACE_LINK_*, ..._LINK_REGISTRAR_*, ..._LINK_PHASE_* |
three link tables | PluginLink.h |
NEVERC_INTERFACE_LTO_*, ..._LTO_REGISTRAR_* |
NevercLTOAPI, NevercLTORegistrarAPI |
PluginLTO.h |
NEVERC_INTERFACE_DYNCODE_*, ..._DYNCODE_REGISTRAR_*, ..._DYNCODE_PHASE_* |
three dyncode tables | PluginDynCode.h |
Each header also defines the matching NEVERC_<DOMAIN>_API_MAJOR and
_MINOR you should pass to QueryInterface.
An interface is either NEVERC_INTERFACE_STABLE (a newer host may only
append) or NEVERC_INTERFACE_LOCKSTEP (target-specific schemas that must
match exactly). Compare the schema digest before consuming LOCKSTEP values.
Register receives a NevercRegistrarAPI and an opaque RegistrarContext:
typedef struct NevercRegistrarAPI {
NevercABITableHeader Header;
NevercRegisterInterfaceFn RegisterInterface;
NevercRegisterPhaseFn RegisterPhase;
NevercRegisterObserverFn RegisterObserver;
NevercRegisterInterceptorFn RegisterInterceptor;
NevercRegisterProviderFn RegisterProvider;
NevercRegisterOptionFn RegisterOption;
} NevercRegistrarAPI;Every one of these takes RegistrarContext as its first argument and a
zero-initialized descriptor as its second. Which call you make is what decides
how the host treats you at the phase:
| Call | Descriptor | Callback | Phase must advertise |
|---|---|---|---|
RegisterObserver |
NevercObserverDescriptor |
NevercPhaseObserverFn |
OBSERVABLE |
RegisterInterceptor |
NevercInterceptorDescriptor |
NevercPhaseInterceptorFn |
INTERCEPTABLE |
RegisterProvider |
NevercProviderDescriptor |
NevercPhaseProviderFn |
REPLACEABLE |
RegisterPhase |
NevercPhaseDescriptor |
— | a plugin-defined ID |
RegisterOption |
NevercOptionDescriptor |
optional Validator |
— |
RegisterInterface |
plain arguments | — | — |
A descriptor that fails structural validation is rejected immediately with
NEVERC_STATUS_INVALID_DESCRIPTOR. The policy check happens when the host
applies the registration: an unknown phase, or one that does not advertise the
policy your call requires, is refused there. Observers are the only kind a
sealed gate accepts.
Domain registrars — NevercIRPassAPI.RegisterPass,
NevercTargetAPI.RegisterTarget, NevercObjectFormatAPI.RegisterFormat, and
the rest — take that same RegistrarContext as their second argument, which
is how the host attributes a registration to your plugin.
typedef struct NevercObserverDescriptor {
NevercABITableHeader Header;
NevercInterfaceID Phase;
NevercObserverPoint Points;
uint32_t Reserved;
NevercPhaseObserverFn Callback;
void *UserData;
NevercDestroyUserDataFn DestroyUserData;
} NevercObserverDescriptor;
typedef NevercStatus(NEVERC_CALL *NevercPhaseObserverFn)(
const NevercPhaseFrame *Frame, NevercObserverPoint Point, void *UserData);Points is a bitmask of NEVERC_OBSERVER_BEFORE (1), NEVERC_OBSERVER_AFTER
(2), and NEVERC_OBSERVER_AFTER_COMMIT (4); it must be non-zero, and the
Point argument tells the callback which one fired. From
pluginsdk/examples/DriverTracePlugin.c:
NevercObserverDescriptor Observer = {0};
Observer.Header = (NevercABITableHeader){
sizeof(Observer), NEVERC_PLUGIN_ABI_MAJOR, NEVERC_PLUGIN_ABI_MINOR, 0};
Observer.Phase = (NevercInterfaceID){NEVERC_PHASE_DRIVER_RAW_ARGUMENTS_HIGH,
NEVERC_PHASE_DRIVER_RAW_ARGUMENTS_LOW};
Observer.Points = NEVERC_OBSERVER_BEFORE | NEVERC_OBSERVER_AFTER;
Observer.Callback = observe_arguments;
Observer.UserData = Process;
Status = Registrar->RegisterObserver(RegistrarContext, &Observer);UserData is passed back untouched. Setting DestroyUserData — present on
every descriptor in this section — makes the host release that memory when the
registration goes away, so a per-registration allocation does not have to be
tracked in Destroy.
typedef struct NevercInterceptorDescriptor {
NevercABITableHeader Header;
NevercInterfaceID Phase;
NevercPhaseInterceptorFn Callback;
void *UserData;
NevercDestroyUserDataFn DestroyUserData;
} NevercInterceptorDescriptor;
typedef NevercStatus(NEVERC_CALL *NevercPhaseInterceptorFn)(
const NevercPhaseFrame *Frame, NevercPhaseContinuation *Continuation,
NevercPhaseResult *OutResult, void *UserData);The continuation is the rest of the chain, and the result is how you report what you did with it:
typedef struct NevercPhaseContinuation {
NevercABITableHeader Header;
NevercInvokeNextFn InvokeNext;
void *Context;
uint64_t Generation;
} NevercPhaseContinuation;
typedef struct NevercPhaseResult {
NevercABITableHeader Header;
NevercPhaseAction Action;
uint32_t Reserved;
NevercArtifactHandle Output;
NevercProofHandle Proof;
} NevercPhaseResult;The three actions are not interchangeable. The host validates the result
against what you actually did and fails the chain with
NEVERC_STATUS_POLICY_VIOLATION on any mismatch:
Action |
InvokeNext |
Output |
Proof |
Also requires |
|---|---|---|---|---|
NEVERC_PHASE_CONTINUE |
called once | empty | empty | — |
NEVERC_PHASE_REPLACE |
not called | set | empty | REPLACEABLE |
NEVERC_PHASE_SKIP |
not called | set | set | SKIPPABLE_WITH_PROOF |
InvokeNext may be called at most once and only on the callback thread — a
second call is a policy violation, and a call from another thread reports
NEVERC_STATUS_WRONG_SCOPE. An interceptor that returns CONTINUE without
having called it is also a policy violation, because the phase would silently
produce nothing.
NevercInterceptorDescriptor Interceptor = {0};
Interceptor.Header = (NevercABITableHeader){
sizeof(Interceptor), NEVERC_PLUGIN_ABI_MAJOR, NEVERC_PLUGIN_ABI_MINOR, 0};
Interceptor.Phase = (NevercInterfaceID){NEVERC_PHASE_DRIVER_EXECUTE_JOB_HIGH,
NEVERC_PHASE_DRIVER_EXECUTE_JOB_LOW};
Interceptor.Callback = intercept_job;
Interceptor.UserData = Process;
Status = Registrar->RegisterInterceptor(RegistrarContext, &Interceptor);A provider replaces a phase outright, so it also declares the determinism contract the build cache relies on:
typedef struct NevercProviderDescriptor {
NevercABITableHeader Header;
NevercInterfaceID Phase;
NevercStringView ProviderID;
NevercPhaseRoute Route;
NevercBool Deterministic;
NevercBool Cacheable;
NevercBool FallbackSafe;
uint32_t Reserved;
NevercPhaseProviderFn Callback;
void *UserData;
NevercDestroyUserDataFn DestroyUserData;
} NevercProviderDescriptor;
typedef NevercStatus(NEVERC_CALL *NevercPhaseProviderFn)(
const NevercPhaseFrame *Frame, NevercPhaseResult *OutResult,
void *UserData);Provider.ProviderID = SV("com.example.my-lowering");
Provider.Route = /* triple / CPU / features / object format */;
Provider.Deterministic = NEVERC_TRUE;
Provider.Cacheable = NEVERC_TRUE;
Provider.FallbackSafe = NEVERC_FALSE; /* built-in cannot silently take over */ProviderID must be a canonical name: at most 255 bytes of lowercase letters,
digits, ., _, and -, not starting or ending with a dot and never
containing ... An upper-case character is enough to have the registration
refused. Route.Header must be initialized like any other table header.
There is no continuation: the callback is the phase. It must report
NEVERC_PHASE_REPLACE with an Output and an empty Proof — anything else is
a policy violation.
FallbackSafe is the one flag with a runtime effect beyond bookkeeping. When
it is NEVERC_TRUE and the provider fails with a status marked
NEVERC_STATUS_FLAG_RECOVERABLE, the host may discard the partial effects and
run the built-in implementation instead. Leave it NEVERC_FALSE when a
half-finished attempt cannot be rolled back.
RegisterPhase adds a transition the host does not know about, which is what
the 8 extension-ID families are reserved for:
typedef struct NevercPhaseDescriptor {
NevercABITableHeader Header;
NevercInterfaceID Phase;
NevercStringView CanonicalName;
NevercInterfaceID InputArtifact;
NevercInterfaceID OutputArtifact;
NevercPhasePolicy Policy;
NevercObserverPoint ObserverPoints;
uint32_t Reserved;
} NevercPhaseDescriptor;Phase, InputArtifact, and OutputArtifact must all be non-zero, and
Policy must be non-zero and contain only known flags. Declaring
ObserverPoints without NEVERC_PHASE_OBSERVABLE is rejected, as is combining
NEVERC_PHASE_SEALED_HOST_GATE with any of INTERCEPTABLE, REPLACEABLE, or
SKIPPABLE_WITH_PROOF — the same invariants the built-in graph is checked
against. Take the ID from your domain's family so it cannot collide with a
future built-in:
#include "neverc/Plugin/Schema/PluginPhaseSchema.inc"
/* NEVERC_EXTENSION_FAMILY_COUNT is 8; family 1 is "neverc.ir.extension". */
NevercInterfaceID MyPhase = {NEVERC_EXTENSION_FAMILY_1_ID_HIGH,
NEVERC_EXTENSION_FAMILY_1_ID_LOW_MIN};Each family publishes _NAMESPACE, _ID_HIGH, _ID_LOW_MIN, and
_ID_LOW_MAX, and the low half is yours to allocate within that range.
RegisterInterface is the one call that takes no descriptor. It hands the host
a table of your own so that another plugin can reach it through the same
QueryInterface used for built-in interfaces:
Registrar->RegisterInterface(RegistrarContext, MyInterfaceID,
NEVERC_INTERFACE_STABLE, &MyTable,
/* Compatibility = */ NULL);Pass NEVERC_INTERFACE_LOCKSTEP instead when the table carries
target-specific schema values that cannot survive a version skew. A lockstep
interface must supply a NevercCompatibilityKey, which pins the consumer to
one producer build:
typedef struct NevercCompatibilityKey {
NevercABITableHeader Header;
NevercStringView ProducerBuildID; /* compare against Bootstrap->HostBuildID */
NevercStringView TargetABIKey;
uint32_t LLVMMajor; /* compare against Bootstrap->LLVMMajor */
uint32_t Reserved;
} NevercCompatibilityKey;All three fields must be populated for a lockstep registration; an empty build ID, an empty ABI key, or a zero LLVM major is rejected as an invalid descriptor.
Include the aggregate header or only the domains you use:
#include "neverc/Plugin/NevercPluginAPI.h" /* everything */
#include "neverc/Plugin/PluginIR.h" /* or one domain */Build a shared module with NeverC itself:
neverc --target=arm64-apple-macosx -shared \
-I/path/to/pluginsdk/include \
-o MyPlugin.dylib MyPlugin.cOr against an installed SDK with CMake:
find_package(NevercPluginSDK REQUIRED)
add_library(my_plugin MODULE my_plugin.c)
target_link_libraries(my_plugin PRIVATE NevercPluginSDK::headers)Or with pkg-config:
cc -shared $(pkg-config --cflags neverc-plugin) -o my_plugin.so my_plugin.cUse .so, .dylib, or .dll as appropriate for the host. The SDK links no
LLVM and no NeverC runtime — NevercPluginSDK::headers is header-only.
neverc -fplugin=./MyPlugin.dylib -c input.c -o input.o| Option | Form | Purpose |
|---|---|---|
-fplugin=<path> |
repeatable | Load a full-toolchain plugin shared module. |
-fplugin-arg=<plugin-id>:<key>=<value> |
repeatable | Pass a namespaced value to a registered plugin option. |
-fplugin-provider=<phase>:<plugin-id> |
repeatable | Select which plugin provides a replaceable phase. |
-fplugin-pass=<dsopath> |
repeatable | Load a C-ABI out-of-tree pass plugin. |
-fplugin-pass-arg=<key>=<value> |
repeatable | Pass an argument to C-ABI pass plugins. |
The <plugin-id>: qualifier may be omitted only when exactly one plugin is
active. Options a plugin registers with RegisterOption are also accepted
directly by their declared spelling, in flag, joined, separate, or
multi-argument form. Plugin arguments and provider selections without a
matching -fplugin= are a hard error rather than a silent no-op.
A registered option is read back at any time through the core table:
uint64_t Count = 0;
Core->GetPluginOptionValueCount(Core->Context, Session, PluginID,
SV("--driver-trace"), &Count);
NevercStringView Value;
Core->GetPluginOptionValue(Core->Context, Session, PluginID,
SV("--driver-trace"), 0, &Value);- Query capability tables through
QueryInterface; require the matching major and checkStructSizebefore touching a field. - Initialize every public structure's
Headerand reserved storage. Zero the struct, then setStructSize,Major,Minor, andFlags. - Treat handles and borrowed views as scoped, opaque values. Never retain a task-scoped handle past its callback, never use it in another session or task, and never manufacture a handle value.
- Return
NevercStatusfrom every callback. Do not let a C++ exception or a host-owned pointer cross the C boundary. - Declare the narrowest truthful
NevercConcurrencyModel(SESSION_SERIAL,THREAD_SAFE,PROCESS_SERIAL) andNevercReentrancyModel(NONE,ALLOWED). - Perform IR, MIR, AST, graph, and artifact changes through the transactional host APIs: begin a mutation, stage changes, then commit or abort. Commit verifies and publishes atomically; a failed commit leaves the previous state intact.
- Allocate through
NevercCoreAPI.Allocate/Reallocate/Deallocatewhen the host should account for the memory. - Keep mutable state in the host-supplied process/session/task state. Global
mutable state is checked by
utils/plugin-api/check-global-state.py.
All public structs are laid out under NEVERC_ABI_PACK_BEGIN (8-byte
packing) with fixed-width types only. New functions are appended to
independently versioned capability tables; the stable prefix of a table does
not change within the first ABI major (NEVERC_PLUGIN_ABI_MAJOR = 1).
NevercStatus carries a Code, Flags, and a Detail word. The complete
code set:
| Code | Meaning |
|---|---|
NEVERC_STATUS_OK |
Success. |
NEVERC_STATUS_INVALID_ARGUMENT |
A required pointer or value was missing or malformed. |
NEVERC_STATUS_ABI_MISMATCH |
The negotiated table is too small or the major differs. |
NEVERC_STATUS_MISSING_INTERFACE |
The host does not publish the requested interface. |
NEVERC_STATUS_VERSION_MISMATCH |
The requested major/minor cannot be satisfied. |
NEVERC_STATUS_INVALID_DESCRIPTOR |
A descriptor failed structural validation. |
NEVERC_STATUS_DUPLICATE_ID |
An ID was already registered. |
NEVERC_STATUS_DEPENDENCY_MISSING |
A declared dependency is absent. |
NEVERC_STATUS_DEPENDENCY_CYCLE |
Registration order cannot be satisfied. |
NEVERC_STATUS_BUSY |
A resource is held elsewhere. |
NEVERC_STATUS_CANCELLED |
Cooperative cancellation was requested. |
NEVERC_STATUS_RESOURCE_EXHAUSTED |
A budget or limit was hit. |
NEVERC_STATUS_STALE_HANDLE |
A handle outlived the object it named. |
NEVERC_STATUS_WRONG_SESSION |
A handle was used in another session. |
NEVERC_STATUS_WRONG_SCOPE |
A handle was used outside its scope. |
NEVERC_STATUS_WRONG_TYPE |
A handle named a different entity kind. |
NEVERC_STATUS_INVALID_STATE |
The operation is not legal in the current state. |
NEVERC_STATUS_POLICY_VIOLATION |
The phase policy forbids the operation. |
NEVERC_STATUS_VERIFICATION_FAILED |
A sealed host verifier rejected the product. |
NEVERC_STATUS_CAPABILITY_UNAVAILABLE |
The host cannot offer the capability here. |
NEVERC_STATUS_PLUGIN_FAILURE |
The plugin reported a generic failure. |
NEVERC_STATUS_PLUGIN_EXCEPTION |
An exception escaped a plugin callback. |
NEVERC_STATUS_OUTPUT_PARTIAL |
The output was written only in part. |
NEVERC_STATUS_REENTRANCY_DENIED |
A reentrant call was refused. |
NEVERC_STATUS_NOT_FOUND |
The named entity does not exist. |
The flag bits describe what happened to the output, which is what a build
system needs to decide whether a retry is safe:
NEVERC_STATUS_FLAG_RECOVERABLE, _OUTPUT_ALREADY_COMMITTED,
_OUTPUT_MAY_BE_PARTIAL, _OUTPUT_RECOVERY_REQUIRED, and
_DURABILITY_UNCONFIRMED.
Report problems with NevercCoreAPI.EmitDiagnostic and a
NevercDiagnosticDescriptor carrying severity (NOTE, REMARK, WARNING,
ERROR, FATAL), code, plugin ID, phase ID, message, notes, source location,
ranges, and fix-its. Call CheckCancelled before expensive work.
Build all of them:
cmake --build build-neverc --target neverc-pluginsdk-examplesEvery example is compiled twice — once with the configured host C compiler and
once with the freshly built NeverC — so the ABI is proven from both sides.
Modules land in build-neverc/neverc/pluginsdk/examples/host/.
| Example | CMake target | Shows |
|---|---|---|
DriverTracePlugin.c |
neverc-plugin-example-driver-trace |
Option registration, phase observation, job interception |
VirtualHeaderPlugin.c |
neverc-plugin-example-virtual-header |
A VFS provider serving an in-memory header |
ASTRewritePlugin.c |
neverc-plugin-example-ast-rewrite |
Parser interception and atomic AST mutation |
ExamplePlugin.c |
neverc-plugin-example-ir-overview |
A module-level IR pass walking the function list with a value cursor |
FunctionPass.c |
neverc-plugin-example-function-pass |
A stable IR function pass |
MachinePass.c |
neverc-plugin-example-machine-pass |
A stable MIR pass at the pre-emit hook |
MCObserverPlugin.c |
neverc-plugin-example-mc-observer |
Read-only MC emission events |
ObjectRewritePlugin.c |
neverc-plugin-example-object-rewrite |
Transactional ObjectGraph rewriting |
CustomCallConvPlugin.c |
neverc-plugin-example-custom-callconv |
Data-driven calling conventions |
DynCodeTracePlugin.c |
neverc-plugin-example-dyncode-trace |
Observing the dyncode pipeline |
DynCodeEncoderPlugin.c |
neverc-plugin-example-dyncode-encoder |
Intercepting dyncode charset encoding |
CrtShimPlugin.c |
neverc-plugin-example-crt-shim |
A plugin with zero CRT dependency |
BenchPlugin.c |
neverc-plugin-example-abi-bench |
ABI call-throughput microbenchmark |
Load one:
neverc -fplugin=build-neverc/neverc/pluginsdk/examples/host/FunctionPass.so \
-O2 -c input.c -o input.o| File | Guarantees |
|---|---|
neverc/include/neverc/Plugin/Schema/PhaseSchema.json |
Phase IDs, policies, stability, verifier gates |
pluginsdk/manifest/plugin.json |
ABI version, interface IDs/versions/stability, schema digests, supported targets |
pluginsdk/abi/plugin.json |
Measured size, alignment, and field offsets of every public struct, per host ABI key |
docs/plugin-api/coverage.json |
Maps each stable phase to positive, negative, replacement, observer, and sealed-gate tests |
An SDK can therefore be validated against a host mechanically, and a plugin build can assert its struct layout against the ABI key it will load into.