Skip to content

WLX Plugin Model

Nikolai Sachok edited this page Jun 24, 2026 · 2 revisions

WLX Plugin Model

Double Commander inherits Total Commander's plugin architecture. A plugin is a native shared library that exports a fixed, documented table of C functions; the host loads it with dlopen and calls those functions by name. There is no IPC, no manifest runtime, no scripting layer — the plugin runs in the host's process.

The four plugin families

Type Extension Role
WLX .wlx Lister — custom file viewers
WCX .wcx Packer — archive formats
WDX .wdx Content — virtual columns / search fields
WFX .wfx File system — virtual/remote volumes

This collection starts with WLX because viewers are where a good macOS rendering stack (WebKit, Quartz, QuickLook) pays off most. The WDX family (content columns) is covered in MediaInfo Content Plugin (WDX).

The WLX entry points

A lister plugin exports exactly these C symbols:

HWND ListLoad(HWND ParentWin, char *FileToLoad, int ShowFlags);
int  ListLoadNext(HWND ParentWin, HWND PluginWin, char *FileToLoad, int ShowFlags);
void ListCloseWindow(HWND ListWin);
void ListGetDetectString(char *DetectString, int maxlen);
void ListSetDefaultParams(ListDefaultParamStruct *dps);

You can confirm a real plugin's exports — and learn by example — with nm:

nm -gU "/Applications/Double Commander.app/Contents/MacOS/plugins/wlx/MacPreview/MacPreview.wlx"
#  _ListLoad  _ListLoadNext  _ListCloseWindow  _ListGetDetectString  _ListSetDefaultParams

What each does

  • ListGetDetectString writes a rule string telling DC which files this plugin claims, e.g. EXT="MD"|EXT="MARKDOWN". DC evaluates registered plugins in order and uses the first whose rule matches — so a plugin with a narrow rule must be registered before a catch-all like the bundled MacPreview's (EXT!="").
  • ListLoad is handed the host's viewer window and a file path. It builds a view that displays the file, attaches it, and returns a handle to it.
  • ListLoadNext reuses the existing plugin view to show the next/previous file as the user navigates inside the viewer — cheaper than tearing down and recreating.
  • ListCloseWindow destroys the view and releases resources.
  • ListSetDefaultParams passes the plugin its interface version and an ini path for settings (optional to use).

The macOS mapping: HWND is NSView*

The ABI was written for Windows, where HWND is a window handle. On macOS, Double Commander maps HWND to a Cocoa NSView*, and __stdcall is a no-op. So a macOS lister:

  1. receives the viewer's NSView* as ParentWin,
  2. creates its own NSView (for markdown-wlx, one hosting a WKWebView),
  3. adds it under the parent and returns it (DC sizes it to the viewer),
  4. tears it down in ListCloseWindow.

Object lifetime across the C boundary (the subtle part)

ListLoad returns an Objective-C object (an NSView) as an opaque HWND, and that object must outlive the function call — DC holds the handle and hands it back later to ListLoadNext / ListCloseWindow. Under ARC, a returned autoreleased view would be deallocated out from under DC.

The fix is to transfer ownership explicitly across the bridge:

// ListLoad: hand a +1-retained pointer out to the C world
return (HWND)CFBridgingRetain(view);

// ListCloseWindow: take ownership back and balance the retain
NSView *view = (NSView *)CFBridgingRelease(ListWin);
[view removeFromSuperview];

CFBridgingRetain / CFBridgingRelease are the ARC-correct way to move an object between ARC and manual-retain (C) worlds. This is the single most important detail to get right in any macOS plugin that returns Cocoa objects through a C ABI.

Why third-party plugins load at all (code signing)

macOS will refuse to load an unsigned/foreign dylib into a process that uses the hardened runtime with library validation. Double Commander is ad-hoc signed with no hardened runtime and no entitlements, so an ad-hoc-signed plugin (codesign --sign -) loads cleanly. Always check the host before assuming you can inject a library:

codesign -dvvv "/Applications/Double Commander.app" 2>&1 | grep -E "flags|Signature"
# flags=0x2(adhoc) ... Signature=adhoc   → no library validation → third-party plugins OK

Continue to Markdown Plugin Architecture for how markdown-wlx fills this skeleton in.

Clone this wiki locally