C# で vJoy の操作を行う (フィーダーを作る)

# vJoy

**vJoy** は、Windows で仮想ジョイスティック (ゲームパッド) を提供するフリーソフトである。

* [Home](https://web.archive.org/web/20190509030358/https://vjoystick.sourceforge.net/site/index.php) (アーカイブ)
* [GitHub - jshafer817/vJoy: Virtual Joystick](https://github.com/jshafer817/vJoy)
* [vJoy - Browse /Beta 2.x/SDK at SourceForge.net](https://sourceforge.net/projects/vjoystick/files/Beta%202.x/SDK/)

この vJoy が提供する仮想ジョイスティックは、**フィーダー**というプログラムから操作を行う。
vJoy では、フィーダーから仮想ジョイスティックを操作するための DLL も提供している。
今回は、C# でこのフィーダーを作成する方法の基礎を紹介する。

# 操作用のDLLの位置を取得する

まず、操作用のDLLを読み込むため、DLLの位置を取得する。
vJoy のSDKのドキュメントより、DLLの位置 (ディレクトリ) はレジストリのキー

```text
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{8E31F76F-74C3-47F1-9550-E041EEDC5FBB}_is1
```

に格納されていることがわかる。
32ビット用のDLLの位置は値 `DllX86Location` に、64ビット用のDLLの位置は値 `DllX64Location` に格納されている。

今実行しているのが32ビットなのか64ビットなのかは、[`IntPtr.Size` プロパティ](https://learn.microsoft.com/ja-jp/dotnet/api/system.intptr.size?view=net-8.0)で判定できる。
このプロパティの値が `4` なら32ビット、`8` なら64ビットである。

レジストリからデータを1個取得するには、[`Registry.GetValue` メソッド](https://learn.microsoft.com/ja-jp/dotnet/api/microsoft.win32.registry.getvalue?view=net-8.0)が便利である。
このメソッドでは、値を取得するキーのパス・値の名前・デフォルト値を指定してデータ読み出しを行う。

```csharp:操作用のDLLがあるディレクトリを取得する
public class GetDllLocation
{
	public static void Main()
	{
		bool is64bit = System.IntPtr.Size == 8;
		object dllLocationRaw = Microsoft.Win32.Registry.GetValue(
			"HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{8E31F76F-74C3-47F1-9550-E041EEDC5FBB}_is1",
			is64bit ? "DllX64Location" : "DllX86Location",
			null
		);
		if (dllLocationRaw is string)
		{
			string dllLocation = (string)dllLocationRaw;
			System.Console.WriteLine(dllLocation);
		}
		else
		{
			System.Console.WriteLine("エラー");
		}
	}
}
```

# 操作用のDLLを読み込む

操作用のDLLは、前章で取得したディレクトリ内にある `vJoyInterface.dll` である。
このDLLを読み込み、関数を使用する準備をする。
DLLに含まれる関数の名前・引数・戻り値については、SDKのドキュメントを参照すること。
`VJD_STAT_*` 系の定数の定義は、`inc/vjoyinterface.h` にある。
`HID_USAGE_*` 系の定数の定義は、`inc/public.h` にある。

DLLを読み込む用のAPIを用意する。
警告を避けるため、`DllImport` を用いた関数宣言は `NativeMethods` クラスの中に入れるのがいいらしい。

```csharp
private static class NativeMethods
{
	[DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "LoadLibraryW")]
	internal static extern IntPtr LoadLibrary([In] string fileName);

	[DllImport("kernel32.dll", CharSet = CharSet.Ansi)]
	internal static extern IntPtr GetProcAddress(IntPtr hModule, [In] string procName);

	 [DllImport("kernel32.dll")]
	[return: MarshalAs(UnmanagedType.Bool)]
	internal static extern bool FreeLibrary(IntPtr hModule);
}
 ```

読み込んだDLLのハンドルを格納する変数と、DLLから関数のアドレスを取得する関数を用意する。

```csharp
private IntPtr hDll;

private T LoadProc<T>(string procName)
{
	IntPtr procAddress = NativeMethods.GetProcAddress(hDll, procName);
	if (procAddress == IntPtr.Zero)
	{
		throw new Exception("failed to find " + procName + "()");
	}
	return (T)(Object)Marshal.GetDelegateForFunctionPointer(procAddress, typeof(T));
}
```

DLLの関数を呼び出すためのデレゲート型・デレゲート変数・デレゲートを呼び出すラッパー関数を用意する。(一例)

```csharp
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate int GetVJDButtonNumberDelegateType(uint rID);
private GetVJDButtonNumberDelegateType GetVJDButtonNumberDelegate;

public int GetVJDButtonNumber(uint rID)
{
	return GetVJDButtonNumberDelegate(rID);
}
```

前章で確認した方法で読み込むべきDLLの位置を取得し、DLLを読み込む。

```csharp
bool is64bit = System.IntPtr.Size == 8;
object dllLocation = Microsoft.Win32.Registry.GetValue(
	"HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{8E31F76F-74C3-47F1-9550-E041EEDC5FBB}_is1",
	is64bit ? "DllX64Location" : "DllX86Location",
	null
);
if (!(dllLocation is string))
{
	throw new Exception("failed to read DLL location");
}
hDll = NativeMethods.LoadLibrary(System.IO.Path.Combine((string)dllLocation, "vJoyInterface.dll"));
if (hDll == IntPtr.Zero)
{
	throw new Exception("failed to load DLL");
}
```

DLLから関数を取得する。(一例)

```csharp
GetVJDButtonNumberDelegate = LoadProc<GetVJDButtonNumberDelegateType>("GetVJDButtonNumber");
```

<details><summary>コード全体 (<code>VJoyFeederApi</code>)</summary>

:::note info
このクラスは今回のデモで使用する関数を用意したものであり、SDKで定義されている関数を網羅していない。
:::

```csharp
using System;
using System.Runtime.InteropServices;

public class VJoyFeederApi : IDisposable
{
	private static class NativeMethods
	{
		[DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "LoadLibraryW")]
		internal static extern IntPtr LoadLibrary([In] string fileName);

		[DllImport("kernel32.dll", CharSet = CharSet.Ansi)]
		internal static extern IntPtr GetProcAddress(IntPtr hModule, [In] string procName);

		[DllImport("kernel32.dll")]
		[return: MarshalAs(UnmanagedType.Bool)]
		internal static extern bool FreeLibrary(IntPtr hModule);
	}

	private IntPtr hDll;

	private T LoadProc<T>(string procName)
	{
		IntPtr procAddress = NativeMethods.GetProcAddress(hDll, procName);
		if (procAddress == IntPtr.Zero)
		{
			throw new Exception("failed to find " + procName + "()");
		}
		return (T)(Object)Marshal.GetDelegateForFunctionPointer(procAddress, typeof(T));
	}

	public enum VjdStat
	{
		VJD_STAT_OWN,
		VJD_STAT_FREE,
		VJD_STAT_BUSY,
		VJD_STAT_MISS,
		VJD_STAT_UNKN
	}

	public enum Axis
	{
		HID_USAGE_X = 0x30,
		HID_USAGE_Y = 0x31,
		HID_USAGE_Z = 0x32,
		HID_USAGE_RX = 0x33,
		HID_USAGE_RY = 0x34,
		HID_USAGE_RZ = 0x35,
		HID_USAGE_SL0 = 0x36,
		HID_USAGE_SL1 = 0x37,
		HID_USAGE_WHL = 0x38
	}

	[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
	[return: MarshalAs(UnmanagedType.Bool)]
	private delegate bool VJoyEnabledDelegateType();
	private VJoyEnabledDelegateType VJoyEnabledDelegate;

	[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
	[return: MarshalAs(UnmanagedType.Bool)]
	private delegate bool DriverMatchDelegateType(ref ushort dllVer, ref ushort drvVer);
	private DriverMatchDelegateType DriverMatchDelegate;

	[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
	private delegate VjdStat GetVJDStatusDelegateType(uint rID);
	private GetVJDStatusDelegateType GetVJDStatusDelegate;

	[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
	[return: MarshalAs(UnmanagedType.Bool)]
	private delegate bool AcquireVJDDelegateType(uint rID);
	private AcquireVJDDelegateType AcquireVJDDelegate;

	[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
	private delegate void RelinquishVJDDelegateType(uint rID);
	private RelinquishVJDDelegateType RelinquishVJDDelegate;

	[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
	private delegate int GetVJDButtonNumberDelegateType(uint rID);
	private GetVJDButtonNumberDelegateType GetVJDButtonNumberDelegate;

	// SDK のドキュメントでは BOOL を返すと主張しているが、実際は以下の値を返すようである (2.1.9)
	// デバイスが存在しない (VJD_STAT_MISS) :  -2
	// デバイスが存在し、軸が存在しない     : -10
	// デバイスが存在し、軸が存在する       :   1
	[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
	private delegate int GetVJDAxisExistDelegateType(uint rID, Axis axis);
	private GetVJDAxisExistDelegateType GetVJDAxisExistDelegate;

	[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
	[return: MarshalAs(UnmanagedType.Bool)]
	private delegate bool SetAxisDelegateType(int value, uint rID, Axis axis);
	private SetAxisDelegateType SetAxisDelegate;

	[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
	[return: MarshalAs(UnmanagedType.Bool)]
	private delegate bool SetBtnDelegateType([MarshalAs(UnmanagedType.Bool)] bool value, uint rID, byte nBtn);
	private SetBtnDelegateType SetBtnDelegate;

	public VJoyFeederApi()
	{
		bool is64bit = System.IntPtr.Size == 8;
		object dllLocation = Microsoft.Win32.Registry.GetValue(
			"HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{8E31F76F-74C3-47F1-9550-E041EEDC5FBB}_is1",
			is64bit ? "DllX64Location" : "DllX86Location",
			null
		);
		if (!(dllLocation is string))
		{
			throw new Exception("failed to read DLL location");
		}
		hDll = NativeMethods.LoadLibrary(System.IO.Path.Combine((string)dllLocation, "vJoyInterface.dll"));
		if (hDll == IntPtr.Zero)
		{
			throw new Exception("failed to load DLL");
		}
		VJoyEnabledDelegate = LoadProc<VJoyEnabledDelegateType>("vJoyEnabled");
		DriverMatchDelegate = LoadProc<DriverMatchDelegateType>("DriverMatch");
		GetVJDStatusDelegate = LoadProc<GetVJDStatusDelegateType>("GetVJDStatus");
		AcquireVJDDelegate = LoadProc<AcquireVJDDelegateType>("AcquireVJD");
		RelinquishVJDDelegate = LoadProc<RelinquishVJDDelegateType>("RelinquishVJD");
		GetVJDButtonNumberDelegate = LoadProc<GetVJDButtonNumberDelegateType>("GetVJDButtonNumber");
		GetVJDAxisExistDelegate = LoadProc<GetVJDAxisExistDelegateType>("GetVJDAxisExist");
		SetAxisDelegate = LoadProc<SetAxisDelegateType>("SetAxis");
		SetBtnDelegate = LoadProc<SetBtnDelegateType>("SetBtn");
	}

	public void Dispose()
	{
		NativeMethods.FreeLibrary(hDll);
	}

	public bool VJoyEnabled()
	{
		return VJoyEnabledDelegate();
	}

	public bool DriverMatch(ref ushort dllVer, ref ushort drvVer)
	{
		return DriverMatchDelegate(ref dllVer, ref drvVer);
	}

	public VjdStat GetVJDStatus(uint rID)
	{
		return GetVJDStatusDelegate(rID);
	}

	public bool AcquireVJD(uint rID)
	{
		return AcquireVJDDelegate(rID);
	}

	public void RelinquishVJD(uint rID)
	{
		RelinquishVJDDelegate(rID);
	}

	public int GetVJDButtonNumber(uint rID)
	{
		return GetVJDButtonNumberDelegate(rID);
	}

	public bool GetVJDAxisExist(uint rID, Axis axis)
	{
		return GetVJDAxisExistDelegate(rID, axis) > 0;
	}

	public bool SetAxis(int value, uint rID, Axis axis)
	{
		return SetAxisDelegate(value, rID, axis);
	}

	public bool SetBtn(bool value, uint rID, byte nBtn)
	{
		return SetBtnDelegate(value, rID, nBtn);
	}
}
```

</details>

:::note info
SDKのドキュメントでは、指定の軸が存在するかを判定する `GetVJDAxisExist` 関数は `BOOL` を返すことになっている。
しかし、実験を行ったところ、(少なくともバージョン 2.1.9 では) 指定の軸が存在しない場合に負の値が返り、`UnmanagedType.Bool` を用いて `bool` 型にマーシャリングすると正しく真偽 (軸の有無) を判定できないことがわかった。
したがって、今回は返り値の型を `int` とし、ラッパー関数で真偽値に変換している。
:::

## 使用した主なAPI

* [DllImportAttribute クラス (System.Runtime.InteropServices) | Microsoft Learn](https://learn.microsoft.com/ja-jp/dotnet/api/system.runtime.interopservices.dllimportattribute?view=net-8.0)
* [UnmanagedFunctionPointerAttribute クラス (System.Runtime.InteropServices) | Microsoft Learn](https://learn.microsoft.com/ja-jp/dotnet/api/system.runtime.interopservices.unmanagedfunctionpointerattribute?view=net-9.0)
* [MarshalAsAttribute クラス (System.Runtime.InteropServices) | Microsoft Learn](https://learn.microsoft.com/ja-jp/dotnet/api/system.runtime.interopservices.marshalasattribute?view=net-8.0)
* [Marshal.GetDelegateForFunctionPointer メソッド (System.Runtime.InteropServices) | Microsoft Learn](https://learn.microsoft.com/ja-jp/dotnet/api/system.runtime.interopservices.marshal.getdelegateforfunctionpointer?view=net-9.0)
* [LoadLibraryW 関数 (libloaderapi.h) - Win32 apps | Microsoft Learn](https://learn.microsoft.com/ja-jp/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibraryw)
* [GetProcAddress 関数 (libloaderapi.h) - Win32 apps | Microsoft Learn](https://learn.microsoft.com/ja-jp/windows/win32/api/libloaderapi/nf-libloaderapi-getprocaddress)
* [FreeLibrary 関数 (libloaderapi.h) - Win32 apps | Microsoft Learn](https://learn.microsoft.com/ja-jp/windows/win32/api/libloaderapi/nf-libloaderapi-freelibrary)

## 参考サイト

* [LoadLibraryを使ってC#からC++DLLを動的ロードして、DLL内の関数を呼び出す方法 - SKSP-TECH](https://sksp-tech.net/archives/492)
* [【Windows/C#】なるべく丁寧にDllImportを使う #.NET - Qiita](https://qiita.com/mitsu_at3/items/94807ee0b3bf34ffb6b2)
* [How to marshal a C++ enum in C# - Stack Overflow](https://stackoverflow.com/questions/22176159/how-to-marshal-a-c-enum-in-c-sharp)

# 操作を行ってみる

vJoy の仮想ジョイスティックの操作は、以下の手順で行う。

1. APIを初期化する
   * vJoy が有効かを確認する
   * vJoy のインターフェイス (DLL) とドライバのバージョンが合っているかを確認する
2. 操作を行うデバイス (仮想ジョイスティック) を選択する
   * デバイスの状態が `VJD_STAT_FREE` であることを確認する
   * デバイスのボタンの数や軸の有無が、アプリケーションが要求する条件に合っていることを確認する
3. デバイスの操作を開始する (acquire)
4. デバイスを操作する (ボタンや軸の状態を設定する)
5. デバイスの操作を終了する (relinquish)

SDKのドキュメントより、仮想ジョイスティックの番号は、1 から 16 まで (両端を含む) であることがわかる。
この例では、条件に合う最初の仮想ジョイスティックのY軸とボタン1を操作する。

```csharp
using System;
using System.Threading;

public class VJoyFeederApiUser
{
	public static void Main(string[] args)
	{
		// APIを初期化する 
		VJoyFeederApi api = new VJoyFeederApi();
		if (!api.VJoyEnabled())
		{
			Console.WriteLine("vJoy is not enabled");
			return;
		}
		ushort dllVer = 0, drvVer = 0;
		if (!api.DriverMatch(ref dllVer, ref drvVer)) {
			Console.WriteLine("DLL version 0x{0:x} != driver version 0x{0:x}", dllVer, drvVer);
			return;
		}
		Console.WriteLine("using vJoy version 0x{0:x}", drvVer);

		// 操作を行うデバイスを選択する
		uint deviceToUse = 0;
		for (uint i = 1; i <= 16; i++) {
			VJoyFeederApi.VjdStat status = api.GetVJDStatus(i);
			int numButtons = api.GetVJDButtonNumber(i);
			bool yAxisExists = api.GetVJDAxisExist(i, VJoyFeederApi.Axis.HID_USAGE_Y);
			Console.WriteLine("device #{0}:", i);
			Console.WriteLine("  status: {0}", status);
			Console.WriteLine("  has {0} button(s)", numButtons);
			Console.WriteLine("  {0} Y axis", yAxisExists ? "has" : "doesn't have");
			if (
				deviceToUse == 0 &&
				status == VJoyFeederApi.VjdStat.VJD_STAT_FREE &&
				numButtons >= 1 &&
				yAxisExists
			)
			{
				deviceToUse = i;
			}
		}
		Console.WriteLine("-----------");
		if (deviceToUse == 0)
		{
			Console.WriteLine("no device to use found");
			return;
		}
		Console.WriteLine("using device #{0}", deviceToUse);

		// デバイスの操作を開始する
		if (!api.AcquireVJD(deviceToUse))
		{
			Console.WriteLine("failed to acquire the device");
			return;
		}

		// デバイスを操作する
		Thread.Sleep(1000);
		Console.WriteLine("moving Y axis to maximum");
		api.SetAxis(0x8000, deviceToUse, VJoyFeederApi.Axis.HID_USAGE_Y);
		Thread.Sleep(1000);
		Console.WriteLine("moving Y axis to minimum");
		api.SetAxis(0x1, deviceToUse, VJoyFeederApi.Axis.HID_USAGE_Y);
		Thread.Sleep(1000);
		Console.WriteLine("moving Y axis to center");
		api.SetAxis(0x4000, deviceToUse, VJoyFeederApi.Axis.HID_USAGE_Y);
		Thread.Sleep(1000);
		Console.WriteLine("pressing button 1");
		api.SetBtn(true, deviceToUse, 1);
		Thread.Sleep(1000);
		Console.WriteLine("releasing button 1");
		api.SetBtn(false, deviceToUse, 1);
		Thread.Sleep(1000);

		// デバイスの操作を終了する
		api.RelinquishVJD(deviceToUse);
	}
}
```

vJoy に付属している「Monitor vJoy」や、[Gamepad Tester](https://hardwaretester.com/gamepad) により、軸やボタンを操作できていることが確認できた。

# まとめ

## 今回行ったこと

* C# でDLLのパスを指定して読み込み、関数を使用する方法を確認した
* vJoy の操作用DLLの関数を読み込み、実際に操作を行った

## 今回行っていない主なこと

* POVスイッチ (≒十字キー) の処理
* FFB (フォースフィードバック) の処理
* vJoy の構成が変化した際のコールバックの受信
* vJoy 公式の C# ラッパーの使用