Skip to content

MediaInfo Content Plugin WDX

Nikolai Sachok edited this page Jun 24, 2026 · 1 revision

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.

What a WDX content plugin is

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);
  • ContentGetSupportedField enumerates the fields the plugin offers (name, type, units). DC builds the column/field picker from this.
  • ContentGetValue is the hot path: given a file path and a field index, write the value into fieldValue and return its type (ft_string, ft_numeric_32, ft_numeric_floating, …) — or a sentinel like ft_fieldempty.
  • ContentGetDetectString is 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.)

One column, every file type: ft_fieldempty

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.

Architecture

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
PDF 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).

Non-blocking: CONTENT_DELAYIFSLOW

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.

Why AVI is parsed by hand

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.

Case study 1 — the crash that only happened inside Double Commander

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.

Case study 2 — the field that was always blank

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.

  1. the one compiled into the plugin (ContentGetDetectString), and
  2. the one written into doublecmd.xml by 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.

See also

  • WLX Plugin Model — the viewer-side ABI and the NSView* / CFBridgingRetain lifetime rules.
  • Building, Testing & CI — the real-artifact harness pattern (here it dlopens the .wdx and drives ContentGetValue directly, including under FPC-style FP traps).