Skip to content

C# Bindings

libVIIPER is a pure C shared library (libVIIPER.dll on Windows, libVIIPER.so on Linux). From C# you call it through P/Invoke — there is no managed wrapper package for the embedded library, but the complete set of declarations is small, stable, and documented below.

Two ways to use VIIPER from C#

  • libVIIPER (this page) — embed the whole USB/USBIP stack in your process via P/Invoke.
  • Viiper.Client — a generated TCP client library that talks to a standalone viiper server over the network. See Client Libraries.

Project setup

Create a regular .NET console/library project and copy the native library next to your output:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
  <ItemGroup Condition="$([MSBuild]::IsOSPlatform('Windows'))">
    <None Include="..\..\dist\libVIIPER\libVIIPER.dll">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
  </ItemGroup>
  <ItemGroup Condition="$([MSBuild]::IsOSPlatform('Linux'))">
    <None Include="..\..\dist\libVIIPER\libVIIPER.so">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
  </ItemGroup>
</Project>

A complete runnable project lives at examples/libVIIPER/csharp/.

General P/Invoke rules

Apply these to every declaration in this reference:

  • [DllImport("libVIIPER", CallingConvention = CallingConvention.Cdecl)] on every function.
  • bool return values and bool parameters need [MarshalAs(UnmanagedType.I1)].
  • Handles (USBServerHandle, Xbox360DeviceHandle, …) are opaque uintptr_t values → use nuint.
  • size_t parameters → use nuint.
  • Strings inside config/meta structs are char*[MarshalAs(UnmanagedType.LPStr)] string?.
  • Callback delegates need [UnmanagedFunctionPointer(CallingConvention.Cdecl)].
  • Keep delegate instances alive in fields or local variables for the lifetime of the server/device they are registered on — the GC cannot see references held by native code. Registering a method group inline (SetXbox360RumbleCallback(handle, RumbleCallback)) creates a temporary delegate that can be collected at any time.
  • Structs passed by value must mirror the C layout exactly with [StructLayout(LayoutKind.Sequential)].
  • Callbacks are invoked from the USBIP server's background threads. Keep them short and thread-safe, and never call into libVIIPER from inside a callback. PCM buffers passed to audio callbacks are only valid during the call — copy the data if you need to keep it.

Constants

Log levels

enum VIIPERLogLevel
{
    Debug = -4,
    Info  = 0,
    Warn  = 4,
    Error = 8,
}

Xbox 360 buttons

static class Xbox360Buttons
{
    public const uint DPadUp    = 0x0001;
    public const uint DPadDown  = 0x0002;
    public const uint DPadLeft  = 0x0004;
    public const uint DPadRight = 0x0008;
    public const uint Start     = 0x0010;
    public const uint Back      = 0x0020;
    public const uint LThumb    = 0x0040;
    public const uint RThumb    = 0x0080;
    public const uint LShoulder = 0x0100;
    public const uint RShoulder = 0x0200;
    public const uint Guide     = 0x0400;
    public const uint A         = 0x1000;
    public const uint B         = 0x2000;
    public const uint X         = 0x4000;
    public const uint Y         = 0x8000;
}

DualShock 4 buttons & D-pad

static class DS4Buttons
{
    public const ushort Square   = 0x0010;
    public const ushort Cross    = 0x0020;
    public const ushort Circle   = 0x0040;
    public const ushort Triangle = 0x0080;
    public const ushort L1       = 0x0100;
    public const ushort R1       = 0x0200;
    public const ushort L2       = 0x0400;
    public const ushort R2       = 0x0800;
    public const ushort Share    = 0x1000;
    public const ushort Options  = 0x2000;
    public const ushort L3       = 0x4000;
    public const ushort R3       = 0x8000;
    public const ushort PS       = 0x0001;
    public const ushort Touchpad = 0x0002;
}

static class DS4DPad
{
    public const byte Up       = 0x01;
    public const byte Down     = 0x02;
    public const byte Left     = 0x04;
    public const byte Right    = 0x08;
}

DS4 D-pad encoding

DS4DeviceState.DPad is a bitmask (the values above), which is what the DS4DPad class here and the docs/devices/dualshock4.md reference use. Be careful not to confuse it with the DS4_DPAD_* constants in libVIIPER.h (e.g. DS4_DPAD_UP = 0x00, DS4_DPAD_NEUTRAL = 0x08) — those are the USB hat-switch encoding used inside the report and must not be OR'd into the field.

DualSense buttons & D-pad

static class DSButtons
{
    public const uint Square   = 0x00000010;
    public const uint Cross    = 0x00000020;
    public const uint Circle   = 0x00000040;
    public const uint Triangle = 0x00000080;
    public const uint L1       = 0x00000100;
    public const uint R1       = 0x00000200;
    public const uint L2       = 0x00000400;
    public const uint R2       = 0x00000800;
    public const uint Create   = 0x00001000;
    public const uint Options  = 0x00002000;
    public const uint L3       = 0x00004000;
    public const uint R3       = 0x00008000;
    public const uint PS       = 0x00010000;
    public const uint Touchpad = 0x00020000;
    public const uint MicMute  = 0x00040000;
    public const uint LFn      = 0x00100000; // Edge
    public const uint RFn      = 0x00200000; // Edge
    public const uint L4       = 0x00400000; // Edge
    public const uint R4       = 0x00800000; // Edge
}

static class DSDPad
{
    public const byte Up    = 0x01;
    public const byte Down  = 0x02;
    public const byte Left  = 0x04;
    public const byte Right = 0x08;
}

static class DSConnection
{
    public const byte Headphone = 0x01;
    public const byte Mic       = 0x02;
    public const byte MicMuted  = 0x04;
    public const byte USBData   = 0x08;
    public const byte USBPower  = 0x10;
}

static class DSShellColors
{
    public const string White = "00";
    public const string Black = "01";
    public const string CosmicRed = "02";
    public const string NovaPink = "03";
    public const string GalacticPurple = "04";
    public const string StarlightBlue = "05";
    public const string GreyCamouflage = "06";
    public const string VolcanicRed = "07";
    public const string SterlingSilver = "08";
    public const string CobaltBlue = "09";
    public const string ChromaTeal = "10";
    public const string ChromaIndigo = "11";
    public const string ChromaPearl = "12";
    public const string Anniversary30th = "30";
    public const string GodOfWarRagnarok = "Z1";
    public const string SpiderMan2 = "Z2";
    public const string AstroBot = "Z3";
    public const string Fortnite = "Z4";
    public const string MonsterHunterWilds = "Z5";
    public const string TheLastOfUs = "Z6";
    public const string GhostOfYotei = "Z7";
    public const string IconBlueLimitedEdition = "ZB";
    public const string AstroBotJoyfulEdition = "ZC";
    public const string GenshinImpact = "ZE";
}

Switch 2 Pro buttons

static class NS2ProButtons
{
    public const uint B          = 0x00000001;
    public const uint A          = 0x00000002;
    public const uint Y          = 0x00000004;
    public const uint X          = 0x00000008;
    public const uint R          = 0x00000010;
    public const uint ZR         = 0x00000020;
    public const uint Plus       = 0x00000040;
    public const uint RightStick = 0x00000080;
    public const uint Down       = 0x00000100;
    public const uint Right      = 0x00000200;
    public const uint Left       = 0x00000400;
    public const uint Up         = 0x00000800;
    public const uint L          = 0x00001000;
    public const uint ZL         = 0x00002000;
    public const uint Minus      = 0x00004000;
    public const uint LeftStick  = 0x00008000;
    public const uint Home       = 0x00010000;
    public const uint Capture    = 0x00020000;
    public const uint GR         = 0x00040000;
    public const uint GL         = 0x00080000;
    public const uint C          = 0x00100000;
    public const uint Headset    = 0x00200000;
}

static class NS2ProSticks
{
    public const ushort Min    = 0x0000;
    public const ushort Center = 0x0800;
    public const ushort Max    = 0x0FFF;
}

Keyboard modifiers & LEDs

static class KBModifiers
{
    public const byte LeftCtrl  = 0x01;
    public const byte LeftShift = 0x02;
    public const byte LeftAlt   = 0x04;
    public const byte LeftGUI   = 0x08;
    public const byte RightCtrl = 0x10;
    public const byte RightShift = 0x20;
    public const byte RightAlt  = 0x40;
    public const byte RightGUI  = 0x80;
}

static class KBLEDs
{
    public const byte NumLock    = 0x01;
    public const byte CapsLock   = 0x02;
    public const byte ScrollLock = 0x04;
    public const byte Compose    = 0x08;
    public const byte Kana       = 0x10;
}

Mouse buttons

static class MouseButtons
{
    public const byte Left    = 0x01;
    public const byte Right   = 0x02;
    public const byte Middle  = 0x04;
    public const byte Back    = 0x08;
    public const byte Forward = 0x10;
}

Keyboard key codes

KB_KEY_* constants are USB HID usage codes (page 0x07, Keyboard/Keypad). Set bits in KeyboardDeviceState.KeyBitmap at the index of the key code divided by 8 (KeyBitmap[code / 8] |= (byte)(1 << (code % 8))).

static class KBKeys
{
    public const byte A = 0x04;   public const byte B = 0x05;
    public const byte C = 0x06;   public const byte D = 0x07;
    public const byte E = 0x08;   public const byte F = 0x09;
    public const byte G = 0x0A;   public const byte H = 0x0B;
    public const byte I = 0x0C;   public const byte J = 0x0D;
    public const byte K = 0x0E;   public const byte L = 0x0F;
    public const byte M = 0x10;   public const byte N = 0x11;
    public const byte O = 0x12;   public const byte P = 0x13;
    public const byte Q = 0x14;   public const byte R = 0x15;
    public const byte S = 0x16;   public const byte T = 0x17;
    public const byte U = 0x18;   public const byte V = 0x19;
    public const byte W = 0x1A;   public const byte X = 0x1B;
    public const byte Y = 0x1C;   public const byte Z = 0x1D;
    public const byte Digit1 = 0x1E; public const byte Digit2 = 0x1F;
    public const byte Digit3 = 0x20; public const byte Digit4 = 0x21;
    public const byte Digit5 = 0x22; public const byte Digit6 = 0x23;
    public const byte Digit7 = 0x24; public const byte Digit8 = 0x25;
    public const byte Digit9 = 0x26; public const byte Digit0 = 0x27;
    public const byte Enter = 0x28;  public const byte Escape = 0x29;
    public const byte Backspace = 0x2A; public const byte Tab = 0x2B;
    public const byte Space = 0x2C;  public const byte Minus = 0x2D;
    public const byte Equal = 0x2E;  public const byte LeftBrace = 0x2F;
    public const byte RightBrace = 0x30; public const byte Backslash = 0x31;
    public const byte Semicolon = 0x33; public const byte Apostrophe = 0x34;
    public const byte Grave = 0x35;  public const byte Comma = 0x36;
    public const byte Period = 0x37; public const byte Slash = 0x38;
    public const byte CapsLock = 0x39;
    public const byte F1 = 0x3A;  public const byte F2 = 0x3B;
    public const byte F3 = 0x3C;  public const byte F4 = 0x3D;
    public const byte F5 = 0x3E;  public const byte F6 = 0x3F;
    public const byte F7 = 0x40;  public const byte F8 = 0x41;
    public const byte F9 = 0x42;  public const byte F10 = 0x43;
    public const byte F11 = 0x44; public const byte F12 = 0x45;
    public const byte PrintScreen = 0x46; public const byte ScrollLock = 0x47;
    public const byte Pause = 0x48; public const byte Insert = 0x49;
    public const byte Home = 0x4A;  public const byte PageUp = 0x4B;
    public const byte Delete = 0x4C; public const byte End = 0x4D;
    public const byte PageDown = 0x4E;
    public const byte Right = 0x4F; public const byte Left = 0x50;
    public const byte Down = 0x51;  public const byte Up = 0x52;
    public const byte NumLock = 0x53; public const byte KeypadSlash = 0x54;
    public const byte KeypadAsterisk = 0x55; public const byte KeypadMinus = 0x56;
    public const byte KeypadPlus = 0x57; public const byte KeypadEnter = 0x58;
    public const byte Keypad1 = 0x59; public const byte Keypad2 = 0x5A;
    public const byte Keypad3 = 0x5B; public const byte Keypad4 = 0x5C;
    public const byte Keypad5 = 0x5D; public const byte Keypad6 = 0x5E;
    public const byte Keypad7 = 0x5F; public const byte Keypad8 = 0x60;
    public const byte Keypad9 = 0x61; public const byte Keypad0 = 0x62;
    public const byte KeypadDot = 0x63;
    public const byte Mute = 0x7F; public const byte VolumeUp = 0x80;
    public const byte VolumeDown = 0x81;
}

See the USB HID Usage Tables for the canonical key-code assignments.

Structs

[StructLayout(LayoutKind.Sequential)]
struct USBServerConfig
{
    [MarshalAs(UnmanagedType.LPStr)]
    public string? addr;                            // default "0.0.0.0:3241"
    public ulong connection_timeout_ms;             // default 30000 (30s)
    public ulong device_handler_connect_timeout_ms; // default 5000 (5s)
    public uint  write_batch_flush_interval_ms;     // default 1 (1ms)
}

All device state structs are passed by value to Set<Device>DeviceState. Zeroed fields select defaults; a zeroed meta struct is equivalent to passing NULL.

[StructLayout(LayoutKind.Sequential)]
struct Xbox360DeviceState
{
    public uint  Buttons;
    public byte  LT;
    public byte  RT;
    public short LX;
    public short LY;
    public short RX;
    public short RY;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)]
    public byte[] Reserved;
}

[StructLayout(LayoutKind.Sequential)]
struct DS4DeviceState
{
    public sbyte  LX, LY, RX, RY;
    public ushort Buttons;
    public byte   DPad;
    public byte   L2, R2;
    public ushort Touch1X, Touch1Y;
    public byte   Touch1Active;
    public ushort Touch2X, Touch2Y;
    public byte   Touch2Active;
    public short  GyroX, GyroY, GyroZ;
    public short  AccelX, AccelY, AccelZ;
}

[StructLayout(LayoutKind.Sequential)]
struct DSDeviceState
{
    public sbyte  LX, LY, RX, RY;
    public uint   Buttons;
    public byte   DPad;
    public byte   L2, R2;
    public ushort Touch1X, Touch1Y;
    public byte   Touch1Active, Touch1Tracking;
    public ushort Touch2X, Touch2Y;
    public byte   Touch2Active, Touch2Tracking;
    public short  GyroX, GyroY, GyroZ;
    public short  AccelX, AccelY, AccelZ;
}

[StructLayout(LayoutKind.Sequential)]
struct NS2ProDeviceState
{
    public uint   Buttons;
    public ushort LX, LY, RX, RY;
    public short  AccelX, AccelY, AccelZ;
    public short  GyroX, GyroY, GyroZ;
}

[StructLayout(LayoutKind.Sequential)]
struct KeyboardDeviceState
{
    public byte Modifiers;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
    public byte[] KeyBitmap; // 256-bit bitmap, one bit per HID key code
}

[StructLayout(LayoutKind.Sequential)]
struct MouseDeviceState
{
    public byte  Buttons;
    public short DX, DY;
    public short Wheel, Pan;
}

Meta structs are passed by pointer; fields left at zero value keep defaults:

[StructLayout(LayoutKind.Sequential)]
struct DS4MetaState
{
    [MarshalAs(UnmanagedType.LPStr)] public string? SerialNumber;
    [MarshalAs(UnmanagedType.LPStr)] public string? Board;
    public byte   BatteryStatus;
    public double TemperatureCelsius;
    public double BatteryVoltage;
    [MarshalAs(UnmanagedType.LPStr)] public string? BuildTime; // RFC3339 or "YYYY-MM-DD HH:MM:SS"
}

[StructLayout(LayoutKind.Sequential)]
struct DSMetaState
{
    [MarshalAs(UnmanagedType.LPStr)] public string? SerialNumber;
    [MarshalAs(UnmanagedType.LPStr)] public string? MACAddress;
    [MarshalAs(UnmanagedType.LPStr)] public string? Board;
    public byte   BatteryStatus;
    public double TemperatureCelsius;
    public double BatteryVoltage;
    [MarshalAs(UnmanagedType.LPStr)] public string? ShellColor; // 2-char code, e.g. "00", "Z1"
    [MarshalAs(UnmanagedType.LPStr)] public string? BuildTime;
    public byte   ConnectionStatus; // DS_CONNECTION_* flags
}

[StructLayout(LayoutKind.Sequential)]
struct NS2ProMetaState
{
    [MarshalAs(UnmanagedType.LPStr)] public string? SerialNumber;
    public byte   BatteryLevel;  // 0-9; 0 = use default
    public byte   Charging;      // 0 = not charging
    public byte   ExternalPower; // 0 = battery only
    public ushort BatteryVolts;  // mV; 0 = use default
}

The full DualSense output state (delivered by the output-state and realtime-haptics callbacks) is passed by value:

[StructLayout(LayoutKind.Sequential)]
struct DSOutputState
{
    public byte RumbleSmall;
    public byte RumbleLarge;
    public byte LedRed, LedGreen, LedBlue;
    public byte PlayerLeds;

    public byte TriggerR2Mode;
    public byte TriggerR2StartResistance;
    public byte TriggerR2EffectForce;
    public byte TriggerR2RangeForce;
    public byte TriggerR2NearReleaseStrength;
    public byte TriggerR2NearMiddleStrength;
    public byte TriggerR2PressedStrength;
    public byte TriggerR2Frequency;

    public byte TriggerL2Mode;
    public byte TriggerL2StartResistance;
    public byte TriggerL2EffectForce;
    public byte TriggerL2RangeForce;
    public byte TriggerL2NearReleaseStrength;
    public byte TriggerL2NearMiddleStrength;
    public byte TriggerL2PressedStrength;
    public byte TriggerL2Frequency;

    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 48)]
    public byte[] RawOutputReport;             // native USB output report 0x02
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 398)]
    public byte[] BluetoothCombinedOutputReport; // V5 carrier 0x36; 64-byte haptics sample at offset 78 (see audio example)

    public byte MicLed;             // 0=off, 1=on, 2=pulse
    public byte LightbarSetup;      // 0x01 default, 0x02 custom
    public byte LightbarBrightness; // 0=high, 1=medium, 2=low
}

[StructLayout(LayoutKind.Sequential)]
struct NS2ProOutputState
{
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
    public byte[] LeftRumble;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
    public byte[] RightRumble;
    public byte Flags;        // bit 0 = rumble update, bit 1 = player LED update
    public byte PlayerLedMask;
}

Callback delegates

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate void VIIPERLogCallbackDelegate(VIIPERLogLevel level, [MarshalAs(UnmanagedType.LPStr)] string message);

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate void Xbox360RumbleCallbackDelegate(nuint handle, byte leftMotor, byte rightMotor);

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate void KeyboardLEDCallbackDelegate(nuint handle, byte leds);

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate void DS4OutputCallbackDelegate(nuint handle, byte rumbleSmall, byte rumbleLarge,
    byte ledRed, byte ledGreen, byte ledBlue, byte flashOn, byte flashOff);

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate void DS4SpeakerCallbackDelegate(nuint handle, IntPtr pcm, nuint length);

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate void DS4SpeakerResetCallbackDelegate(nuint handle);

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate void DSOutputCallbackDelegate(nuint handle, byte rumbleSmall, byte rumbleLarge,
    byte ledRed, byte ledGreen, byte ledBlue, byte playerLeds);

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate void DSOutputStateCallbackDelegate(nuint handle, DSOutputState output);

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate void DSRealtimeHapticsCallbackDelegate(nuint handle, DSOutputState output);

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate void DSSpeakerResetCallbackDelegate(nuint handle);

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate void DSAudioCallbackDelegate(nuint handle, IntPtr pcm, nuint length);

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate void NS2ProOutputCallbackDelegate(nuint handle, NS2ProOutputState output);

PCM callbacks (DS4SpeakerCallbackDelegate, DSAudioCallbackDelegate) receive a raw pointer that is only valid during the call. Copy it with Marshal.Copy:

DS4SpeakerCallbackDelegate speakerCb = (handle, pcm, length) =>
{
    var buf = new byte[(int)length];
    Marshal.Copy(pcm, buf, 0, buf.Length);
    // use buf ...
};

P/Invoke declarations

Server lifecycle

static class LibVIIPER
{
    const string Lib = "libVIIPER";

    [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.I1)]
    public static extern bool NewUSBServer([In] ref USBServerConfig config,
        out nuint outHandle, VIIPERLogCallbackDelegate? logCallback);

    [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.I1)]
    public static extern bool CloseUSBServer(nuint handle);

    [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.I1)]
    public static extern bool CreateUSBBus(nuint serverHandle, ref uint busID);

    [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.I1)]
    public static extern bool RemoveUSBBus(nuint serverHandle, uint busID);

Xbox 360

    [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.I1)]
    public static extern bool CreateXbox360Device(nuint serverHandle, out nuint outDeviceHandle,
        uint busID, [MarshalAs(UnmanagedType.I1)] bool autoAttachLocalhost,
        ushort idVendor, ushort idProduct, byte xinputSubType);

    [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.I1)]
    public static extern bool SetXbox360DeviceState(nuint deviceHandle, Xbox360DeviceState state);

    [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.I1)]
    public static extern bool SetXbox360RumbleCallback(nuint deviceHandle, Xbox360RumbleCallbackDelegate? callback);

    [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.I1)]
    public static extern bool RemoveXbox360Device(nuint deviceHandle);

DualShock 4

    [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.I1)]
    public static extern bool CreateDS4Device(nuint serverHandle, out nuint outDeviceHandle,
        uint busID, [MarshalAs(UnmanagedType.I1)] bool autoAttachLocalhost,
        ushort idVendor, ushort idProduct, ref DS4MetaState meta);

    [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.I1)]
    public static extern bool SetDS4DeviceState(nuint deviceHandle, DS4DeviceState state);

    [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.I1)]
    public static extern bool SetDS4MetaState(nuint deviceHandle, ref DS4MetaState meta);

    [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.I1)]
    public static extern bool SetDS4OutputCallback(nuint deviceHandle, DS4OutputCallbackDelegate? callback);

    [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.I1)]
    public static extern bool SetDS4SpeakerCallback(nuint deviceHandle, DS4SpeakerCallbackDelegate? callback);

    [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.I1)]
    public static extern bool SetDS4SpeakerResetCallback(nuint deviceHandle, DS4SpeakerResetCallbackDelegate? callback);

    [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.I1)]
    public static extern bool SetDS4MicrophonePCM(nuint deviceHandle, [In] byte[] data, nuint length);

    [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.I1)]
    public static extern bool RemoveDS4Device(nuint deviceHandle);

DualSense (and Edge)

    // Variants: Device, EdgeDevice, AudioOnlyDevice, EdgeAudioOnlyDevice,
    //           GamepadOnlyDevice, EdgeGamepadOnlyDevice
    [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.I1)]
    public static extern bool CreateDualSenseDevice(nuint serverHandle, out nuint outDeviceHandle,
        uint busID, [MarshalAs(UnmanagedType.I1)] bool autoAttachLocalhost,
        ushort idVendor, ushort idProduct, ref DSMetaState meta);

    [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.I1)]
    public static extern bool CreateDualSenseEdgeDevice(nuint serverHandle, out nuint outDeviceHandle,
        uint busID, [MarshalAs(UnmanagedType.I1)] bool autoAttachLocalhost,
        ushort idVendor, ushort idProduct, ref DSMetaState meta);

    [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.I1)]
    public static extern bool CreateDualSenseAudioOnlyDevice(nuint serverHandle, out nuint outDeviceHandle,
        uint busID, [MarshalAs(UnmanagedType.I1)] bool autoAttachLocalhost,
        ushort idVendor, ushort idProduct, ref DSMetaState meta);

    [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.I1)]
    public static extern bool CreateDualSenseEdgeAudioOnlyDevice(nuint serverHandle, out nuint outDeviceHandle,
        uint busID, [MarshalAs(UnmanagedType.I1)] bool autoAttachLocalhost,
        ushort idVendor, ushort idProduct, ref DSMetaState meta);

    [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.I1)]
    public static extern bool CreateDualSenseGamepadOnlyDevice(nuint serverHandle, out nuint outDeviceHandle,
        uint busID, [MarshalAs(UnmanagedType.I1)] bool autoAttachLocalhost,
        ushort idVendor, ushort idProduct, ref DSMetaState meta);

    [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.I1)]
    public static extern bool CreateDualSenseEdgeGamepadOnlyDevice(nuint serverHandle, out nuint outDeviceHandle,
        uint busID, [MarshalAs(UnmanagedType.I1)] bool autoAttachLocalhost,
        ushort idVendor, ushort idProduct, ref DSMetaState meta);

    [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.I1)]
    public static extern bool SetDualSenseDeviceState(nuint deviceHandle, DSDeviceState state);

    [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.I1)]
    public static extern bool SetDualSenseMetaState(nuint deviceHandle, ref DSMetaState meta);

    [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.I1)]
    public static extern bool SetDualSenseOutputCallback(nuint deviceHandle, DSOutputCallbackDelegate? callback);

    [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.I1)]
    public static extern bool SetDualSenseOutputStateCallback(nuint deviceHandle, DSOutputStateCallbackDelegate? callback);

    [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.I1)]
    public static extern bool SetDualSenseRealtimeHapticsCallback(nuint deviceHandle, DSRealtimeHapticsCallbackDelegate? callback);

    [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.I1)]
    public static extern bool SetDualSenseSpeakerResetCallback(nuint deviceHandle, DSSpeakerResetCallbackDelegate? callback);

    [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.I1)]
    public static extern bool SetDualSenseAudioOutCallback(nuint deviceHandle, DSAudioCallbackDelegate? callback);

    [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.I1)]
    public static extern bool SetDualSenseMicrophonePCM(nuint deviceHandle, [In] byte[] data, nuint length);

    [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.I1)]
    public static extern bool RemoveDualSenseDevice(nuint deviceHandle);

Switch 2 Pro

    [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.I1)]
    public static extern bool CreateNS2ProDevice(nuint serverHandle, out nuint outDeviceHandle,
        uint busID, [MarshalAs(UnmanagedType.I1)] bool autoAttachLocalhost,
        ushort idVendor, ushort idProduct, ref NS2ProMetaState meta);

    [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.I1)]
    public static extern bool SetNS2ProDeviceState(nuint deviceHandle, NS2ProDeviceState state);

    [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.I1)]
    public static extern bool SetNS2ProOutputCallback(nuint deviceHandle, NS2ProOutputCallbackDelegate? callback);

    [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.I1)]
    public static extern bool RemoveNS2ProDevice(nuint deviceHandle);

Keyboard

    [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.I1)]
    public static extern bool CreateKeyboardDevice(nuint serverHandle, out nuint outDeviceHandle,
        uint busID, [MarshalAs(UnmanagedType.I1)] bool autoAttachLocalhost,
        ushort idVendor, ushort idProduct);

    [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.I1)]
    public static extern bool SetKeyboardDeviceState(nuint deviceHandle, KeyboardDeviceState state);

    [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.I1)]
    public static extern bool SetKeyboardLEDCallback(nuint deviceHandle, KeyboardLEDCallbackDelegate? callback);

    [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.I1)]
    public static extern bool RemoveKeyboardDevice(nuint deviceHandle);

Mouse

    [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.I1)]
    public static extern bool CreateMouseDevice(nuint serverHandle, out nuint outDeviceHandle,
        uint busID, [MarshalAs(UnmanagedType.I1)] bool autoAttachLocalhost,
        ushort idVendor, ushort idProduct);

    [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.I1)]
    public static extern bool SetMouseDeviceState(nuint deviceHandle, MouseDeviceState state);

    [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.I1)]
    public static extern bool RemoveMouseDevice(nuint deviceHandle);
}

Optional meta parameters

Create/meta functions taking ref DS4MetaState / ref DSMetaState / ref NS2ProMetaState accept NULL in C. In C#, pass a zeroed struct — every zeroed field selects the device default, which is exactly what NULL means.

Complete example

using System.Runtime.InteropServices;

class Program
{
    static void LogCallback(VIIPERLogLevel level, string message)
        => Console.WriteLine($"libVIIPER [{level}] {message}");

    static void RumbleCallback(nuint handle, byte leftMotor, byte rightMotor)
        => Console.WriteLine($"<- Rumble: Left={leftMotor}, Right={rightMotor}");

    static int Main()
    {
        // Keep delegates alive for the lifetime of the server/device!
        VIIPERLogCallbackDelegate logCb = LogCallback;

        USBServerConfig conf = new() { addr = "localhost:3245" };
        if (!LibVIIPER.NewUSBServer(ref conf, out nuint serverHandle, logCb))
            return 1;

        uint busID = 0;
        LibVIIPER.CreateUSBBus(serverHandle, ref busID);

        if (!LibVIIPER.CreateXbox360Device(serverHandle, out nuint deviceHandle, busID,
                autoAttachLocalhost: true, 0, 0, 0))
            return 1;

        Xbox360RumbleCallbackDelegate rumbleCb = RumbleCallback;
        LibVIIPER.SetXbox360RumbleCallback(deviceHandle, rumbleCb);

        Xbox360DeviceState state = new()
        {
            Buttons = Xbox360Buttons.A,
            LT = 128,
            LX = 20000,
        };
        LibVIIPER.SetXbox360DeviceState(deviceHandle, state);

        Console.WriteLine("Press Enter to exit...");
        Console.ReadLine();

        LibVIIPER.CloseUSBServer(serverHandle);
        return 0;
    }
}

DualSense audio example

The DualSense exposes a full audio stack: the host streams speaker/haptics PCM to an audio-out callback, and you feed captured microphone PCM back to the device. All PCM callbacks receive a raw pointer that is only valid during the call, so copy the data with Marshal.Copy before using it elsewhere.

// Keep all delegates alive for the lifetime of the device!
DSAudioCallbackDelegate audioOutCb = (handle, pcm, length) =>
{
    // 4 channels S16LE @ 48 kHz: front L/R + rear haptics L/R
    var bytes = new byte[(int)length];
    Marshal.Copy(pcm, bytes, 0, bytes.Length);
    Console.WriteLine($"<- Audio out: {bytes.Length} bytes");
};

DSSpeakerResetCallbackDelegate speakerResetCb = handle =>
{
    // Stream generation barrier: flush queued speaker PCM / haptics here.
    Console.WriteLine("<- Speaker stream reset");
};

DSOutputStateCallbackDelegate outputStateCb = (handle, output) =>
{
    Console.WriteLine($"<- Rumble: {output.RumbleSmall}/{output.RumbleLarge}, " +
        $"LED: #{output.LedRed:X2}{output.LedGreen:X2}{output.LedBlue:X2}, " +
        $"Players: 0x{output.PlayerLeds:X2}");
    // output.RawOutputReport (48 bytes) and
    // output.BluetoothCombinedOutputReport (398 bytes) are also available —
    // see the realtime haptics callback below for decoding the latter.
};

DSMetaState meta = new() { SerialNumber = "MY-DS-0001" };
if (!LibVIIPER.CreateDualSenseDevice(serverHandle, out nuint dsHandle, busID,
        autoAttachLocalhost: true, 0, 0, ref meta))
    return 1;

LibVIIPER.SetDualSenseAudioOutCallback(dsHandle, audioOutCb);
LibVIIPER.SetDualSenseSpeakerResetCallback(dsHandle, speakerResetCb);
LibVIIPER.SetDualSenseOutputStateCallback(dsHandle, outputStateCb);

// Capture host-facing mic audio and feed it to the virtual device.
// The frame must be exactly 1920 bytes (480 frames of 2ch S16LE @ 48 kHz);
// the call returns false for any other length.
var micFrame = new byte[1920];
// ... fill micFrame from your audio capture ...
LibVIIPER.SetDualSenseMicrophonePCM(dsHandle, micFrame, (nuint)micFrame.Length);

// The V5 combined Bluetooth carrier is a single 398-byte HID report
// (report ID 0x36) that bundles the output state, the haptics sample, and the
// speaker lane:
//
//   [0]      report ID (0x36)
//   [1]      rolling sequence nibble ((seq & 0x0F) << 4)
//   [2..10]  packet 0x11 header: 0x91 0x07 0xFE + 5 queue-length bytes + seq
//   [11]     0x90  (packet 0x10: output state marker)
//   [12]     63    (state length)
//   [13..75] output state (mirrors USB output report bytes 1-47 when set)
//   [76]     0x92  (packet 0x12: haptics marker)
//   [77]     64    (haptics sample length)
//   [78..141] haptics sample: signed 8-bit stereo, 32 frames @ 3 kHz
//   [142]    0x93  (packet 0x13: speaker marker)
//   [143]    0     (speaker length — empty in VIIPER)
//   [394..397] CRC-32 (little-endian) over bytes 0..393 — custom seed, not
//   standard IEEE CRC-32; do not verify it with a generic CRC-32 library

// Low-latency haptics lane: fires as soon as a 512-frame haptics interval
// completes (~93.75 Hz) and carries the fresh 64-byte haptics sample embedded
// in the combined carrier above.
DSRealtimeHapticsCallbackDelegate realtimeHapticsCb = (handle, output) =>
{
    byte[] report = output.BluetoothCombinedOutputReport;
    if (report.Length < 142 || report[0] != 0x36)
        return; // not a V5 combined carrier — nothing to decode

    // Sanity-check the packet 0x12 marker and length before decoding.
    if (report[76] != 0x92 || report[77] != 64)
        return;

    // 64 signed 8-bit samples, stereo-interleaved (L/R): 32 per channel.
    Span<byte> raw = report.AsSpan(78, 64);
    Span<int> left  = stackalloc int[32];
    Span<int> right = stackalloc int[32];
    double l2 = 0, r2 = 0;
    for (int i = 0; i < 32; i++)
    {
        left[i]  = (sbyte)raw[i * 2];     // rear-left haptics actuator
        right[i] = (sbyte)raw[i * 2 + 1]; // rear-right haptics actuator
        l2 += left[i]  * left[i];
        r2 += right[i] * right[i];
    }
    double lRms = Math.Sqrt(l2 / 32) / 127.0; // normalize to 0..1
    double rRms = Math.Sqrt(r2 / 32) / 127.0;
    Console.WriteLine($"<- Realtime haptics: L={lRms:P0} R={rRms:P0}");

    // output.RumbleSmall/RumbleLarge, trigger effect fields and the lightbar
    // are also decoded for you in the struct — no need to parse bytes 13..75.
};

LibVIIPER.SetDualSenseRealtimeHapticsCallback(dsHandle, realtimeHapticsCb);

// Also available: SetDualSenseOutputCallback (simple rumble/LED/player LEDs)
// and the audio-only / gamepad-only / Edge create variants. The Go-only
// atomic speaker+haptics carrier (SetAtomicAudioHapticsCallback) is not
// exposed by the C API — C# gets haptics via the audio-out callback above
// and this realtime lane.

DualShock 4 audio example

The DualShock 4 exposes the same speaker/microphone stack as the DualSense, at different sample rates: the speaker callback receives two S16LE channels at 32 kHz, and microphone frames must be exactly 320 bytes (160 frames of one S16LE channel at 16 kHz).

// Keep all delegates alive for the lifetime of the device!
DS4SpeakerCallbackDelegate speakerCb = (handle, pcm, length) =>
{
    // 2 channels S16LE @ 32 kHz
    var bytes = new byte[(int)length];
    Marshal.Copy(pcm, bytes, 0, bytes.Length);
    Console.WriteLine($"<- Speaker: {bytes.Length} bytes");
};

DS4SpeakerResetCallbackDelegate speakerResetCb = handle =>
{
    // Stream generation barrier: flush queued speaker PCM here.
    Console.WriteLine("<- Speaker stream reset");
};

DS4OutputCallbackDelegate outputCb = (handle, rumbleSmall, rumbleLarge,
    ledRed, ledGreen, ledBlue, flashOn, flashOff) =>
{
    Console.WriteLine($"<- Rumble: {rumbleSmall}/{rumbleLarge}, " +
        $"LED: #{ledRed:X2}{ledGreen:X2}{ledBlue:X2}, Flash: {flashOn}/{flashOff}");
};

DS4MetaState meta = new() { SerialNumber = "MY-DS4-0001" };
if (!LibVIIPER.CreateDS4Device(serverHandle, out nuint ds4Handle, busID,
        autoAttachLocalhost: true, 0, 0, ref meta))
    return 1;

LibVIIPER.SetDS4SpeakerCallback(ds4Handle, speakerCb);
LibVIIPER.SetDS4SpeakerResetCallback(ds4Handle, speakerResetCb);
LibVIIPER.SetDS4OutputCallback(ds4Handle, outputCb);

// Feed captured mic audio to the virtual device. The frame must be exactly
// 320 bytes (160 frames of 1ch S16LE @ 16 kHz); the call returns false for
// any other length.
var micFrame = new byte[320];
// ... fill micFrame from your audio capture ...
LibVIIPER.SetDS4MicrophonePCM(ds4Handle, micFrame, (nuint)micFrame.Length);

Never call into libVIIPER from a callback

Audio/output callbacks run on the USBIP server's background threads. Do not call Set* / Create* / Remove* functions from inside them — marshal the work to your own thread instead.

See also the device reference pages for the exact semantics of every input state, meta state and callback: