Plugin C ABI
A stable C ABI lets a DLL add new node types — with their own persistence, plan/3D rendering, properties and commands — without recompiling yu3d.
Header: sdk/include/yu3d_plugin.h. Host loader:
src/pluginhost/.
Entry point & versioning
#define YU3D_PLUGIN_ABI_VERSION 1u YU3D_PLUGIN_EXPORT int yu3d_plugin_register(const Yu3dHostApi* host);
A plugin exports exactly one function. The host passes a stable function-pointer
table (Yu3dHostApi) valid for the process lifetime; the plugin
returns 1 to accept. Compatibility is additive — new fields are appended to the
end of Yu3dHostApi; check presence with
YU3D_HOST_HAS(host, field) (a struct-size guard) before calling
anything added after 1.0. Incompatible changes bump the major version.
Registering a node type
typedef struct {
const char* type_id; // "acme.pole" — globally unique, stable
Yu3dWriteFn write; // serialise payload
Yu3dReadFn read; // deserialise -> payload
Yu3dDestroyFn destroy; // free payload
Yu3dPlanRenderFn plan_render; // nullable — draw in Plan
Yu3dSceneRenderFn scene_render; // nullable — draw in 3D
void* user; // echoed to callbacks
} Yu3dNodeType;
The payload is an opaque blob the plugin owns; the host stores it
inside a node and hands it back to every callback. Opaque handles
(Yu3dWriter*, Yu3dReader*, Yu3dPlanCtx*,
Yu3dSceneCtx*, Yu3dDocument*) are valid only for the
duration of the call — never store them.
What the host offers
| Area | Functions |
|---|---|
| Binary I/O | w_u8/w_u32/w_f64/w_str; r_u8/r_u32/r_f64/r_str (reader returns 1 on success). |
| Plan render | plan_add_segment/marker/text, plan_world_per_pixel, plan_set_pick_id (sub-entity picking). Colours linear 0..1; text offsets in DIPs. |
| 3D render | scene_add_lines, scene_add_triangles (flat xyz arrays + index arrays). |
| Document | doc_add_node(type_id, payload, name) — wraps a payload in a node under root as one undoable edit. |
| Registration | register_property (typed, get/set), register_summary, register_command(id, label, group, run). |
| Civil model — build | align_create + align_set_geometry_ex (tangent/arc/spiral) + align_set_profile; align_create_offset / align_set_bond (live derived alignments); surface_create + surface_add_breakline; pointgroup_create (COGO points); assembly_create_preset; corridor_create + corridor_bind_assembly + corridor_add_baseline (multi-baseline); run_command; doc_save / doc_save_exchange. |
| Civil model — read | align_kind/category, align_h_element(_count), align_v_pvi(_count), align_elevation_at, align_bond; surface_point/triangle/breakline getters; corridor_baseline/region getters; pointgroup_point/name/code getters. Enough to reconstruct everything the Civil 3D bridge serialises. |
Editable properties
A plugin describes properties (Yu3dPropertyDesc: key, label, kind
REAL/INT/TEXT/BOOL, read-only, get/set callbacks). The Properties dock edits
any module/plugin node through the same path, with undo.
Building & reading the civil model
Beyond registering its own node types, a plugin can author and introspect the
core civil model — alignments (with spirals), design profiles, live derived
alignments (offset / curb-return / connected, carrying a recomputable
bond), surfaces, and multi-baseline corridors — then serialise the
whole thing (doc_save / doc_save_exchange). The
build calls (align_*, surface_create,
corridor_*) route through the host's undo stack; the read calls
expose the same geometry the exchange writer sees. This surface is
dogfooded by the yu3d_api_civil_rt smoke, which builds a
project purely through the ABI, reads every piece back through the ABI, and
asserts parity with the .yu3dx export — so an external plugin can
reconstruct exactly what the Civil 3D bridge does without touching the model.
ExternNode vs ForeignNode
| Plugin loaded | Plugin absent | |
|---|---|---|
| Node class | data::ExternNode | data::ForeignNode |
| Holds | Live opaque payload + destroy callback | Raw payload bytes |
| Behaviour | Renders, edits, persists via the plugin | Inert, but round-trips byte-for-byte |
On load, the plugin host registers a NodeIO for each plugin type that
bridges the C ABI read/write to the engine's BinaryReader/Writer;
a payload becomes an ExternNode. With the plugin missing, the same
bytes load as a ForeignNode: the host keeps them verbatim and
writes them back on save, so a document never loses a plugin's data just
because the plugin is absent. The calls that read and write those bytes are
the payload writer / reader groups.
Loading & the trust gate
// PluginHost — UI-toolkit-free void set_load_approver(function<bool(const string& dll_path)> approver); int load_plugins(const string& dir); // scans *.dll, returns count loaded
The approver is consulted before LoadLibrary — so a DLL's
DllMain cannot run without consent. The policy (trusted hashes,
prompt on first sight / file change) lives host-side; without an approver all
plugins load (the dev default). DLLs stay resident for the process lifetime.
For the safe, in-process extension tier (no native code), see Lua scripting.