Skip to content

Transport: DLL

In DLL transport mode, HydroSym loads your plugin as a native DLL into its own process using LoadLibrary. Communication happens through a single exported C function and a callback function pointer.

Entry point signature

Your DLL must export exactly one function:

__declspec(dllexport) void __cdecl HydroSymPlugin_Invoke(
    const char* method,
    const char* requestJson,
    void (*callback)(int resultCode, const char* responseJson, void* ctx),
    void* ctx
);

C# equivalent (the SDK generates this automatically from HydroSymPluginBase):

[UnmanagedCallersOnly(EntryPoint = "HydroSymPlugin_Invoke", CallConvs = [typeof(CallConvCdecl)])]
public static unsafe void Invoke(
    byte* method,
    byte* requestJson,
    delegate* unmanaged[Cdecl]<int, byte*, void*, void> callback,
    void* ctx)
{
    PluginHost.Invoke(method, requestJson, callback, ctx);
}

The SDK's PluginHost handles the dispatch. You never write this code manually.

Callback signature

typedef void (*ResponseCallback)(
    int resultCode,       // 0=OK, 1=ERROR, 2=NOT_SUPPORTED
    const char* responseJson,
    void* ctx             // same value passed to Invoke
);

The plugin must call the callback exactly once per Invoke call, before Invoke returns.

Memory ownership rules

┌────────────────────────────────────────────────────┐
│ HydroSym owns:                                     │
│   method          → valid until Invoke returns     │
│   requestJson     → valid until Invoke returns     │
│   ctx             → opaque, pass back unchanged    │
├────────────────────────────────────────────────────┤
│ Plugin owns:                                       │
│   responseJson    → must remain valid until        │
│                     the callback returns           │
└────────────────────────────────────────────────────┘

Do not hold pointers to method or requestJson after Invoke returns. Copy them if you need them later. The SDK copies both before returning from Invoke.

The response JSON buffer must outlive the callback call. The SDK manages this with a pinned buffer that is released after the callback returns.

Threading model

  • HydroSym calls Invoke from its main UI thread.
  • The callback must be called from the same thread that called Invoke (i.e., before Invoke returns).
  • Async operations (database calls, HTTP requests) are run on thread pool threads internally, but the result is marshaled back to satisfy the synchronous callback requirement.
  • Do not hold any UI lock (e.g., Application.DoEvents loops) that could cause a deadlock while waiting for an async operation.

Calling convention

The entry point uses __cdecl calling convention (not stdcall). This is the default for C functions on x64 Windows. NativeAOT with [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] produces the correct convention.

stdcall mismatch

Using stdcall (the old TFMS_ convention) with the new entry point causes a stack corruption crash on 64-bit Windows. Always use cdecl / UnmanagedCallersOnly.

String encoding

All strings (method, requestJson, responseJson) are null-terminated UTF-8. JSON strings within the payload may contain Unicode characters (escaped as \uXXXX per RFC 8259, or as raw UTF-8 bytes). The SDK handles encoding transparently.

Debugging

To debug a plugin in Visual Studio:

  1. Open the plugin project in Visual Studio.
  2. Set Debugger Type to Native Only in project properties (required for NativeAOT).
  3. Start HydroSym normally (or use the HydroSymPluginTester.exe for faster iteration).
  4. Debug → Attach to Process → select HydroSym.exe.
  5. Set breakpoints in your plugin code — they will be hit when HydroSym calls your plugin.

For HydroSymPluginTester: 1. Launch HydroSymPluginTester.exe --dll YourPlugin.dll --call GetInfo from Visual Studio's debugger (set it as the startup project with command-line arguments).

Common pitfalls

Problem Cause Fix
EntryPointNotFoundException on load Entry point missing or wrong name Check dumpbin /exports YourPlugin.dll for HydroSymPlugin_Invoke
Crash on first call Wrong calling convention Ensure [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
Garbled JSON response String encoding mismatch Let the SDK handle UTF-8 conversion; never use Marshal.StringToHGlobalAnsi
Plugin loads but nothing happens Capability not listed Add the capability to GetInfo.capabilities
Deadlock on database call Async/await on wrong scheduler Use ConfigureAwait(false) on all awaits in plugin code