Writing a pwsafe Transport Plugin

See parent: pwsafe | ABI spec: transport | Reference impl: file-plugin

Minimal skeleton

A plugin is a shared library (.so/.dylib/.dll) with a single exported entry point. The PWS_EXPORT macro handles the platform difference:

#include "transport.h"

// --- Identity string (must survive link-time optimisation) ---
// Format: PWS_TRANSPORT_INFO:<abi>:<schemes>:<description>
// The loader scans raw bytes before dlopen — no load needed to verify.
#if defined(_MSC_VER)
extern "C" __declspec(dllexport) const char pws_transport_id[] =
    "PWS_TRANSPORT_INFO:1:myscheme:My transport";
#pragma comment(linker, "/INCLUDE:pws_transport_id")
#elif defined(__APPLE__)
__asm__(".section __TEXT,__cstring,cstring_literals\n"
        ".string \"PWS_TRANSPORT_INFO:1:myscheme:My transport\"\n"
        ".previous\n");
#else
__attribute__((used)) static const char pws_transport_ident[] =
    "PWS_TRANSPORT_INFO:1:myscheme:My transport";
#endif

static int my_fetch(const char *url, const char *local_path) {
    // download url -> local_path
    return 0;  // or errno on failure
}
static int my_store(const char *local_path, const char *url) {
    return 0;
}
static int my_exists(const char *url) {
    return ENOENT;
}
static int my_lock(const char *url, char *token_out, size_t len) {
    return ENOTSUP;  // no locking — transport layer proceeds without lock
}
static int my_unlock(const char *url, const char *token) {
    return 0;
}
static void my_cleanup(void) { }

static PWSTransport t = {
    PWSTransport_ABI_VERSION, "myscheme",
    my_fetch, my_store, my_exists, my_lock, my_unlock, my_cleanup
};

extern "C" PWS_EXPORT void pws_plugin_init(pws_register_fn_t reg) {
    reg(&t);
}

Identity string — why three variants?

The loader scans raw bytes (memmem) before dlopen to verify the plugin. The string must be present literally in the binary.

• Linux/ELF: __attribute__((used)) static const char — optimizer might strip it without used

• macOS/Mach-O: inline asm puts it in __TEXT,__cstring which is always retained

• MSVC: __declspec(dllexport) forces the symbol into the export table (.rdata); #pragma comment(linker, "/INCLUDE:") prevents the linker from discarding it

Exporting pws_plugin_init

On Windows/MSVC, extern "C" alone does NOT export a symbol from a DLL. You must use PWS_EXPORT (which expands to __declspec(dllexport) on Windows, empty on Unix/macOS). Without it, GetProcAddress(handle, "pws_plugin_init") returns null and the plugin silently fails to load.

Locking

Return ENOTSUP from lock() if your transport has no server-side locking. The transport layer treats ENOTSUP as 'proceed without a lock, warn once'.

If you implement locking: store the token internally keyed by URL. The caller always passes an empty string for token to unlock() — look it up from your own map. See the WebDAV plugin for the pattern.

Calling context on Unix/macOS

IMPORTANT: once the first lock is acquired, a child process is forked. All subsequent transport calls (fetch, store, lock, unlock) run in the child, not the parent. The child inherits your .so but NOT any state set by the parent after fork.

On Windows there is no fork — all calls happen in the same process, so this does not apply.

Return codes

• 0 — success

• ENOENT — resource not found (404)

• EBUSY — locked by another client (423)

• ENOTSUP — operation not supported (lock/unlock not available)

• EACCES — authentication failure (401/403)

• EIO — I/O or protocol error

Note: MSVC does not define ENOTSUP. transport.h provides #define ENOTSUP 252 as a fallback.

Multiple schemes from one .so/.dll

List all schemes comma-separated in the identity string: PWS_TRANSPORT_INFO:1:https,http:WebDAV transport

Unix/macOS: create a symlink — pwsafe-http.so -> pwsafe-https.so

Windows: copy the DLL — pwsafe-http.dll (symlinks require admin rights on NTFS by default)

CMakeLists.txt snippet

add_library(pwsafe-myscheme MODULE transport-myscheme.cpp)
set_target_properties(pwsafe-myscheme PROPERTIES
    PREFIX ""
    CXX_STANDARD 17)
if(WIN32)
  set_target_properties(pwsafe-myscheme PROPERTIES SUFFIX ".dll")
endif()
target_link_libraries(pwsafe-myscheme PRIVATE <your-deps>)

Building for CI (no cmake)

See Makefile.transport-tests — it builds plugins directly with CXX and handles platform differences:

• Linux: -shared -fPIC + pkg-config --libs libcurl

• macOS: -bundle -undefined dynamic_lookup + curl-config --libs

• Windows (CI): cl /LD + libcurl.lib from vcpkg

macOS codesigning

If the pwsafe.app binary has Hardened Runtime enabled, macOS enforces library validation — it will refuse to load a plugin not signed with the same Team ID. The macos-build.sh script handles this automatically: it signs the plugin with the app's identity and re-signs the bundle with a disable-library-validation entitlement.

For standalone test binaries (no Hardened Runtime), codesigning is not required.

Testing pattern

Gate live-server sections behind an env var, skip with exit 77 if unset:

# Linux/macOS:
make -f Makefile.transport-tests build-plugins build
MYSCHEME_TEST_URL=myscheme://server/path make -f Makefile.transport-tests suite2

Reference implementations: file:// plugin (simplest)https/http plugin (full locking)

version 2  ·  updated 2026-06-04