-
Notifications
You must be signed in to change notification settings - Fork 0
MediaInfo Content Plugin WDX
Where markdown-wlx is a viewer (it returns an NSView),
media-info-wdx is a content plugin (it returns data fields). This page covers
the WDX content ABI, how the plugin is built on native macOS frameworks, and two
host-dependent bugs that only reproduced inside Double Commander — both worth studying.
A WDX plugin lets Double Commander show arbitrary per-file values in custom columns and tooltips. The host calls the plugin once per file/field to fill each cell. The exported C ABI is small:
int ContentGetSupportedField(int n, char *name, char *units, int maxlen);
int ContentGetValue(char *fileName, int field, int unit,
void *fieldValue, int maxlen, int flags);
void ContentGetDetectString(char *detectString, int maxlen);
int ContentGetSupportedFieldFlags(int n);
void ContentSetDefaultParams(ContentDefaultParamStruct *dps);
void ContentPluginUnloading(void);-
ContentGetSupportedFieldenumerates the fields the plugin offers (name, type, units). DC builds the column/field picker from this. -
ContentGetValueis the hot path: given a file path and a field index, write the value intofieldValueand return its type (ft_string,ft_numeric_32,ft_numeric_floating, …) — or a sentinel likeft_fieldempty. -
ContentGetDetectStringis a rule ((EXT="JPG")|(EXT="PNG")|…) limiting which files the plugin applies to. (See the detect-string case study below — there's a subtlety about which detect string DC actually honors.)
The design goal was a single Summary column that adapts per file — image →
3024 × 4032, video → 1920 × 1080 · 12:30, audio → 3:45, pdf → 14 pages. The
mechanism is simple: for a file a field doesn't apply to, ContentGetValue returns
ft_fieldempty and DC renders nothing. So the same column is dense for media and
blank for everything else — no wasted column real estate.
Routing is by extension → category → backend, all native system frameworks, no third-party libraries and no network:
| Category | Fields | Backend |
|---|---|---|
| Image | Dimensions, Width, Height, Megapixels, DPI, bit depth | ImageIO — header read only, never decodes pixels |
| Audio | Duration, Bitrate, Sample rate, Channels, Audio codec | AVFoundation |
| Video (mp4/mov/…) | Dimensions, Duration, Frame rate, Bitrate, codecs | AVFoundation |
| Video (avi) | Dimensions, Duration, Frame rate | self-contained RIFF avih reader |
| Page count | CoreGraphics / CGPDF |
Parsed values are cached per path + mtime in a thread-safe NSCache (DC may call
ContentGetValue from a background content thread concurrently with the UI thread).
AVFoundation parsing is the only slow path. When DC calls in the foreground with the
CONTENT_DELAYIFSLOW flag, the plugin returns ft_delayed instead of parsing;
DC then re-queries the value on a background thread (without the flag) and the plugin
computes it. The panel never stalls while scrolling. ImageIO, CGPDF, and the AVI
reader are fast and answer immediately.
AVFoundation can't open AVI on macOS, and Spotlight had no cached dimensions for old
camera clips. But AVI is a RIFF container whose avih main header carries
dimensions and frame timing at fixed offsets, so the plugin reads the first 64 KB,
finds the avih chunk, and pulls dwWidth/dwHeight/dwMicroSecPerFrame/
dwTotalFrames directly — resolution, frame rate, and duration with no decoder.
Browsing a folder of 2008 Canon JPEGs crashed DC with an "Access violation" the
moment the column rendered. The stack pointed inside Apple's RawCamera.bundle,
reached from ImageIO while copying image properties (parsing the EXIF MakerNotes).
Yet the same CGImageSourceCopyPropertiesAtIndex call over those exact files in a
standalone test process never crashed.
The difference is the host. Double Commander is a Lazarus/FPC application, and the FPC runtime enables floating-point exception traps (invalid / divide-by-zero / overflow). Apple's RAW code does FP math that is harmless under the default masked environment but raises a trap under FPC's — which DC surfaces as an access violation.
This was proven deterministically by toggling the FPU control register the way FPC
does (arm64 FPCR bits 8–10):
| Condition | Result |
|---|---|
| Normal process (traps masked) | reads all 339 JPEGs fine |
| FP traps enabled (like FPC/DC) | crashes with a signal |
| Traps enabled + masked around the ImageIO call | reads all 339 fine |
The fix is to mask FP exceptions across every system-framework call and restore the host's environment before returning:
fenv_t hostEnv;
fegetenv(&hostEnv);
fesetenv(FE_DFL_ENV); // mask: default (non-trapping) FP environment
@try {
info = ParseImage(url); // ImageIO / AVFoundation / CGPDF run here
} @finally {
fesetenv(&hostEnv); // leave DC's environment exactly as we found it
}This is the same lesson as the markdown-wlx Escape-key fix: host-dependent
behavior must be verified in the real host, because a standalone test passes while
the app fails. The regression is locked in by a harness that enables the FPC-style
traps and asserts the plugin survives and restores the environment.
After the crash was fixed, AVI files showed no data, while MP4 worked. The plugin
returned correct values when called directly, so the gap was in how DC invoked it.
A debug build that logged every ContentGetValue call was decisive: 940 calls, every
one a .jpg — DC never queried the plugin for a single .avi.
The cause: there are two detect strings.
- the one compiled into the plugin (
ContentGetDetectString), and - the one written into
doublecmd.xmlby the registration script.
DC gates content-plugin calls by the registered string in doublecmd.xml, and the
registrar's extension list was missing AVI. (Worse, an early sanity check used a
substring test — "AVI" in detectString — which was a false positive because the
image type AVIF contains the substring "AVI".)
Two takeaways: keep the registered detect string and the compiled one in sync, and
check exact clauses ((EXT="AVI")), never substrings. When a plugin returns data
but a column is blank, a one-line debug log of every ContentGetValue(path, field, flags) immediately shows whether the host is even calling you.
-
WLX Plugin Model — the viewer-side ABI and the
NSView*/CFBridgingRetainlifetime rules. -
Building, Testing & CI — the real-artifact harness
pattern (here it
dlopens the.wdxand drivesContentGetValuedirectly, including under FPC-style FP traps).