VirtualDJ's C++ plugin interface: the native-code extension point, and — more importantly for this repo — the boundary where VDJScript's return values are still typed.
Written 2026-07-30. Source labels follow Evidence Standards: the
headers are Official (Tier 2 — official documentation, which this repo has caught being
incomplete before), the on-disk observations are Local observation on VirtualDJ 2026
(bundle 18.0.9482, macOS arm64), and the binary findings are Binary symbol table.
The HTTP control interface returns everything as text, so a query's real type has to be recovered by observation. The plugin SDK shows why: the host exposes two separately typed query entry points, and HTTP collapses both into a response body.
struct IVdjCallbacks8
{
virtual HRESULT SendCommand(const char *command)=0; // execute
virtual HRESULT GetInfo(const char *command, double *result)=0; // numeric query
virtual HRESULT GetStringInfo(const char *command, void *result, int size)=0; // text query
virtual HRESULT DeclareParameter(void *parameter, int type, int id,
const char *name, const char *shortName,
float defaultvalue)=0;
virtual HRESULT GetSongBuffer(int pos, int nb, short **buffer)=0;
};VDJScript is not stringly typed. Script goes in as const char *; results come out as
either a double or a UTF-8 buffer, chosen by which call you make. Everything numeric —
booleans, counts, 0..1 sliders, percentages — shares the single double. That is why
tests/verb-return-types.json records 334 "bool" verbs: those are doubles that the HTTP layer
renders as yes/no.
This maps exactly onto the vtable slots
extract_action_contracts.py calibrated from compiled
ACTION_ classes, before these headers were consulted — independent corroboration of the
same three-way split (Evidence Standards rule 1g):
| Verb-table vtable slot | SDK callback |
|---|---|
| slot 2 — execute | SendCommand |
| slot 3 — generic query (the variant) | GetInfo → double * |
| slot 4 — specialized text query | GetStringInfo → buffer |
It also localises a known defect. master_beat_num returns float32 bits through an integer
path over HTTP (HTTP Control Interface); since the core hands
back a double here, that coercion is almost certainly in the rendering path, not the
engine. A plugin calling GetInfo("master_beat_num", &d) settles it in one call — see
TODO.md task 10.
Worth stating plainly, because the headers look like they should be a reference and are not.
Checked mechanically 2026-07-30 against the 1,028-name verb table: across all 32 KB of unique
header text, not one verb name appears as a verb. Sixteen names match as ordinary English
in comments and C++ parameter names — // the crossfader moves continuously…,
float *volume in OnTransformPosition, // load and unload a plugin,
OnSearch(const char* search, …) — and that is the entire overlap.
This is structural rather than an omission. The command parameter is opaque:
virtual HRESULT GetInfo(const char *command, double *result)=0;The SDK defines how to ask — calling convention, type channels, HRESULT contract — and says nothing about what may be asked. The vocabulary lives entirely inside VirtualDJ, which is why the verb set had to be extracted from the binary (Undocumented VDJScript Candidates §verb table) rather than read out of a header.
Consequences worth keeping straight:
- The plugin is not a discovery instrument for verbs. It is for types and behaviour that only exist at runtime. Existence, aliases, categories and capability are already settled from the binary.
- Verb questions can be answered with no VirtualDJ present — from this repo's committed
artifacts (
tests/verb-table.json,action-contracts.json,verb-return-types.json,verb-existence-sweep.json, all read byjust get-verb <name>). Producing them requires the app; consuming them does not.
| Fact | Detail |
|---|---|
| Copyright | (c)Atomix Productions 2011-2018, in every header |
| License | None stated. No LICENSE text in the headers, and no terms of use or redistribution conditions on the SDK download page. |
| Official download | PluginSDK8.html → VirtualDJ8_SDK_20211003.zip ("header files for all types of plug-ins") |
| Community examples | github.com/szemek/virtualdj-plugins-examples — 8 buildable examples, Xcode 14.3 / macOS 13.3, "copied and adapted from official documentation". No LICENSE file; the headers are still Atomix's. |
Publishing the SDK for plugin authors implies permission to use it; it grants nothing about
redistribution. This repo therefore does not vendor the headers — vendor/ is gitignored,
with fetch instructions in the ignore entry. Put a local copy in vendor/vdj-sdk/ to build
anything described here.
Everything derives from IVdjPlugin8, which supplies lifecycle, parameters, UI, and the three
host callbacks above.
class IVdjPlugin8
{
virtual HRESULT VDJ_API OnLoad() {return S_OK;}
virtual HRESULT VDJ_API OnGetPluginInfo(TVdjPluginInfo8 *info) {return E_NOTIMPL;}
virtual ULONG VDJ_API Release() {delete this; return S_OK;}
virtual HRESULT VDJ_API OnParameter(int id) {return S_OK;}
virtual HRESULT VDJ_API OnGetParameterString(int id, char *outParam, int outParamSize);
virtual HRESULT VDJ_API OnGetUserInterface(TVdjPluginInterface8 *pluginInterface);
IVdjCallbacks8 *cb;
};| Subclass | Header | Key virtuals |
|---|---|---|
IVdjPluginStartStop8 |
vdjPlugin8.h |
OnStart, OnStop |
IVdjPluginDsp8 |
vdjDsp8.h |
OnProcessSamples(float *buffer, int nb) |
IVdjPluginBufferDsp8 |
vdjDsp8.h |
buffered audio variant |
IVdjPluginPositionDsp8 |
vdjDsp8.h |
OnTransformPosition(double *songPos, double *videoPos, float *volume, float *srcVolume) |
IVdjPluginVideoFx8 |
vdjVideo8.h |
OnDraw, OnDeviceInit, OnDeviceClose, GetDevice, GetTexture |
IVdjPluginVideoTransition8 |
vdjVideo8.h |
OnDraw(float crossfader) |
IVdjPluginVideoTransitionMultiDeck8 |
vdjVideo8.h |
OnDrawMultiDeck(int nbVideoDecks, int *videoDecks) |
IVdjPluginOnlineSource |
vdjOnlineSource.h |
OnSearch, GetStreamUrl, GetFolder, GetFolderList, OnLogin/OnOAuth, context menus |
OnTransformPosition and the online-source interface are worth noting for this repo: they
expose song position and browser/source integration at a level VDJScript does not reach.
Typeinfo names present in /Applications/VirtualDJ.app/Contents/MacOS/VirtualDJ but absent
from every public header (Binary symbol table, 2026-07-30):
IVdjPluginHelperBeats— unexplored. The name is suggestive given that beatgrids and waveforms are a standing gap in this repo.IVdjTransitionCallbacks8
The public surface is a subset of the live one. Treat the SDK as a partial export of an internal framework, not a complete description of it.
DeclareParameter is called during OnLoad(); typed wrappers exist for each kind.
| Constant | Value | Wrapper / notes |
|---|---|---|
VDJPARAM_BUTTON |
0 | DeclareParameterButton(int *) |
VDJPARAM_SLIDER |
1 | DeclareParameterSlider(float *, …, float defaultvalue) |
VDJPARAM_SWITCH |
2 | DeclareParameterSwitch(int *, …, bool) |
VDJPARAM_STRING |
3 | DeclareParameterString(char *, …, int parameterSize) |
VDJPARAM_CUSTOM |
4 | DeclareParameterCustom(void *, …, int parameterSize) |
VDJPARAM_RADIO |
5 | DeclareParameterRadio(int *) |
VDJPARAM_COMMAND |
6 | DeclareParameterCommand(char *) |
VDJPARAM_COLORFX |
7 | slider with default 0.5; one per effect, single-knob full control |
VDJPARAM_BEATS |
8 | float, number of beats |
VDJPARAM_BEATS_RELATIVE |
9 | int; set ±1 to step the beat count when OnParameter fires |
VDJPARAM_POSITION |
10 | float[4]; user resize/position in a video plugin's GUI |
VDJPARAM_RELEASEFX |
11 | pairs with the release-FX model in Effects Engines |
VDJPARAM_TRANSITIONFX |
12 | name fixed to "Transition FX" / "Trans" |
Declared parameters are persisted to a per-plugin .ini beside the plugin, in an
[autoparams] section of <Type> <Name> <id>=<value>:
[autoparams]
Slider Strength 0=1925 ; native_Blur.ini
String Nudge 2=0.0 ms ; native_Ableton Link.ini
Custom HttpPort 1=50000000 ; native_Network Control.ini
SString Auth 2=This is the same framework VirtualDJ is built with. 173 native_*.ini manifests ship in
~/Library/Application Support/VirtualDJ/PluginsMacArm/ — Blur, Backspin, Beat Grid, Slicer,
Ableton Link and Network Control all declare parameters this way. Type-name frequency across
all of them: Slider ×487, Switch ×220, Custom ×11, SString ×6, Position ×5,
String ×3.
Note SString — used for the Network Control plugin's Auth password field, and not a
documented VDJPARAM_ constant. Another piece of internal surface that did not make it into
the public header. (Its meaning is unconfirmed; "secure string" is a guess from its one known
use, not a claim.)
NODOCK 0x1 · PROCESSAFTERSTOP 0x2 · PROCESSFIRST 0x4 · PROCESSLAST 0x8 ·
EXTENSION1 0x10 (set by VirtualDJ when the passed struct is the extended type) ·
SETPREVIEW 0x20 · POSITION_NOSLIP 0x40 · ALWAYSPREFADER 0x80 · ALWAYSPOSTFADER 0x100 ·
EPHEMERAL 0x200 (do not save/load params from the ini).
This matters for Evidence Standards rule 1b, which treats absence from the verb table as disproof: if plugins could register verbs, the table would only be the built-in set.
The public SDK has no verb-registration call. Every registration entry point it offers is a
parameter declaration — DeclareParameter plus its thirteen typed wrappers. There is no
DeclareAction, RegisterVerb, AddCommand, or equivalent, in any of the four headers.
Instead, plugin capability is surfaced through existing verbs that take a name or index:
| A plugin adds | Reached from VDJScript by |
|---|---|
| An effect (DSP / video FX / transition) | effect_select "Blur", effect_slider, effect_button — by name or slot index |
| Effect-specific commands | effect_command — "send a command to this effect" |
| Declared parameters | the effect slider/button verbs, by id |
| An online source | browser entries, not verbs |
Consistent with that, every plugin-related name in the verb table is a built-in verb for
plugins, not a verb from one: handshake, get_plugindeck, pluginsongpos,
show_pluginpage, effect_command. And with two Beatport online-source plugins installed on
the test machine, the table contains no beatport/tidal/soundcloud/deezer/spotify
name at all.
Scope limit, stated honestly. This is evidence about the public SDK and the stock app. Native plugins are compiled into the binary, so anything they contribute is already in the table. But the public headers are a known subset of the live interface (see the internal interfaces above), so an internal registration path cannot be ruled out from headers alone. Rule 1b should be read as: complete for a stock VirtualDJ plus any plugin using the public API. The test that would settle the rest is a built plugin probing for an undocumented registration path — queued with the introspection plugin.
handshake is a real verb (verb table id 84, category system) and it autocompletes in
VirtualDJ's editor. The bundled catalog documents it (Official, languages.zip):
Perform an encrypted handshake to ensure that this plugin is currently being called by a real VirtualDJ environment. Call this passing any string, decrypt the result using VirtualDJ's handshake public key, and check that it matches what you passed.
Observed over HTTP (Local test, VirtualDJ 2026, 2026-07-30):
| Call | Result |
|---|---|
handshake (bare) |
E_INVALIDARG — argument required |
handshake 'nonce' |
exactly 128 bytes of binary (not UTF-8), i.e. a 1024-bit RSA block |
| same nonce, repeated | byte-identical — deterministic, so no random padding |
| different nonce | completely different 128 bytes |
So a plugin verifies it is talking to genuine VirtualDJ by having it sign a challenge with a private key the plugin checks against a bundled public key.
Worth knowing before building a shim. Because handshake is reachable through the HTTP
control channel, any process that can reach that port can have VirtualDJ sign arbitrary
challenges — a signing oracle. A fake host could therefore relay a plugin's challenge to a real
VirtualDJ and return a valid response, which is precisely the attack the handshake exists to
prevent. This is directly relevant to the shim-server work in
VDJScript Local Test Tracker: a shim that needs to
satisfy a plugin's handshake can proxy it, rather than needing the key. Recorded as an
architectural observation about local software on the tester's own machine; the practical
exposure is bounded by the same thing that bounds the whole channel — see the security note in
HTTP Control Interface.
OnGetUserInterface fills TVdjPluginInterface8, whose Type selects one of three models:
| Type | Value | Meaning |
|---|---|---|
VDJINTERFACE_DEFAULT |
0 | VirtualDJ builds the UI from the declared parameters |
VDJINTERFACE_SKIN |
1 | the plugin supplies skin XML + a PNG — the same skin dialect documented in Skin SDK |
VDJINTERFACE_DIALOG |
2 | native window: HWND on Windows, NSWindow * on macOS |
The skin path is the interesting one for this repo: a plugin hands VirtualDJ an XML buffer and an image buffer, so plugin GUIs and skins share a rendering vocabulary.
The documented entry point is COM-style class-object negotiation:
VDJ_EXPORT HRESULT VDJ_API DllGetClassObject(const GUID &rclsid, const GUID &riid, void **ppObject);with CLSID_VdjPlugin8 plus a per-type IID (IID_IVdjPluginBasic8, IID_IVdjPluginStartStop8,
IID_IVdjPluginDsp8, IID_IVdjPluginBuffer8, IID_IVdjPluginOnlineSource,
IID_IVdjPluginVideoFx8, IID_IVdjPluginVideoTransition8,
IID_IVdjPluginVideoTransitionMultiDeck8). The host string DllGetClassObject does appear in
the VirtualDJ binary.
Open question — do not assume the documented path is the only one. The bundled
beatport16_vdj.bundle (an online-source plugin) exports 11,303 symbols and none is
DllGetClassObject, nor does it contain any SDK GUID as raw bytes. Nor do those GUID byte
sequences appear in the host binary — consistent with the compiler materialising constant GUIDs
inline, so that part is unremarkable. But a shipped online-source plugin with no documented
entry point suggests a second, newer loading path. Unresolved; needs a built test plugin to
settle.
~/Library/Application Support/VirtualDJ/ contains both Plugins64/ and PluginsMacArm/
(same contents on this machine), with per-category subfolders: AutoStart, OnlineSources,
SoundEffect, VideoEffect, VideoTransition, Visualisations. Bundles sit at the top level
(*.bundle), and native_*.ini manifests for built-in plugins sit alongside them.
It is not linked from the wiki's main navigation and surfaces mainly in the context of the
(also obscure) Network Control plugin. The shape of the evidence — headers copyrighted
2011-2018, an interface versioned 8 (the VirtualDJ 8 era), a zip packaged in 2021, 173
first-party plugins built on it, and at least two interfaces plus one parameter type that were
never exported — reads as an internal framework that was published quietly and partially,
rather than a product-managed public API.
Practical consequence: the interface is stable and heavily dogfooded (Atomix's own effects depend on it), which makes it a reasonable foundation to build tooling against.
A read-only introspection plugin is queued as the next instrument for TODO.md task 10, because it can do what the HTTP channel structurally cannot:
- call
GetInfoandGetStringInfoon each of the 955 verbs and read native types plus raw HRESULTs, giving a definitive per-verb type-path map instead of one inferred from rendered text; - probe argument forms in-process, making the 301-verb optional-argument queue and the 217 binary-discovered keyword sets tractable at loop speed rather than HTTP round-trip speed;
- run without the Network Control plugin or a Pro license — a fifth Tier-1 channel with different prerequisites from the existing four.
Caveat that survives the channel change: unrecognised arguments are silently ignored rather than rejected, so keyword confirmation still needs prepared state where forms would differ, not an error code.