HaskellでもDirectX12したい！


<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">描けた！ <a href="https://t.co/3h0Jtmlw7U">pic.twitter.com/3h0Jtmlw7U</a></p>&mdash; 🌌S.Percentage🙀 (@Pctg_x8) <a href="https://twitter.com/Pctg_x8/status/1305842423738720260?ref_src=twsrc%5Etfw">September 15, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

リポジトリはこちら: https://github.com/Pctg-x8/haskell-d3d12-test

## HaskellのFFI

今回はDirectX12に主軸をおいた記事ではないため、DirectX12の初期化の方法などは解説しません。他の記事をご参照ください。

Haskellも実用的なプログラミング言語の例に漏れず、C言語など他の言語とのインターフェイス（FFI）の仕組みを持っています。
Haskellのそれはかなり「低レベル」かつ「なんでもあり」な感じになっています。
HaskellのFFIの詳細については他の記事を参照してもらうとして、大雑把には以下のような形でインターフェイスが提供されています。

### 基本データ型

`Foreign.C.Types` に `CInt` や `CFloat` などといった形で基本的な型の定義が存在するので、それを使用します。

アーキテクチャのポインタサイズと一致する整数型 （`intptr_t` とか）は以下のものが用意されています。これらはどちらも `Foreign.Ptr` に定義されています。

* `IntPtr`: 符号付き
* `WordPtr`: 符号なし

### ポインタ型

ポインタ型は2つの型が用意されています。いずれも `Foreign.Ptr` に定義されています。

* `Ptr a`: いわゆる普通のポインタ
* `FunPtr a`: 関数ポインタ専用

### 構造体

構造体については手動でマーシャリング（メモリ上での配置情報と、メモリへの書き込みと読み取り）のコードを書いてあげる必要があります。hsc2hsとかを使えばある程度自動化したりできるみたいですが、今回は使っていません。
マーシャリングは `Foreign.Storable` にある `Storable` クラスのインスタンスをもって定義します。

```haskell:Foreign.Storable
class Storable a where
  sizeOf :: a -> Int
  alignment :: a -> Int
  peek :: Ptr a -> IO a
  poke :: Ptr a -> a -> IO ()
```

型からも分かる通り、 `peek` が読み取り操作で `poke` が書き込み操作です。これらはバリバリ副作用を含むためIOとなっています。
`sizeOf` が構造体のサイズ、 `alignment` が必要なアラインメント指定です。 `sizeOf` `alignment` は対象データを引数として取る形をしていますが、大抵は使われないため `sizeOf _ = 8` みたいにして書きます。使う際も `sizeOf (undefined :: a)` で、大抵の場合は `a` のサイズを取得できます。
ちなみに、今回見事にハマったのですが、手書きする場合 `sizeOf` はアラインメントを適用したサイズを返す必要があります。

### 配列

配列を取り扱う方法についてはいろいろありますが、大抵は「`[a]` を毎回マーシャリングする」か「`StorableArray` を使う」かになります。 `[a]` はC言語の配列と内部表現が全然違いますので、そのまま使用することはできません。
`StorableArray` は内部表現がそのままC言語の配列のものになるため繰り返し使う際のパフォーマンスに優れますが、呼び出し側での準備が面倒だったりGCをうまく騙したりする必要があります。

### 参照を渡す

COMインターフェイスの生成関数では、返り値が `HRESULT` （エラー値）になっているため、実体を受け取るためにメモリを確保してそのポインタを渡してあげる必要があります。
また、大きな構造体はそのまま関数の引数として渡すことはできないため、ここでも特定のデータへの参照を作り出してそれを渡してあげる必要があります。

未初期化のメモリ領域を確保する場合は、`Foreign.Marshal.Alloc` の `alloca :: Storable a => (Ptr a -> IO b) -> IO b` を使用します。
メモリの確保と同時に任意のデータをマーシャリングしてほしい場合は、 `Foreign.Marshal.Utils` の `with :: Storable a => a -> (Ptr a -> IO b) -> IO b` を使用します。
`CreateHeap` などの関数から、インターフェイスを得る部分に渡すものの場合は前者、 `D3D12_HEAP_DESC` などを渡す場合は後者を使用します。
関数の型からもわかる通り、これらの関数はbracketパターンに即していますのでアクション実行の終了をもってメモリが開放されます。

### 関数

#### 普通の関数をimportする

`foreign import` を使用して、例えば `RegisterClassExA` 関数を持ってくる場合は以下のような形になります。

```haskell
foreign import ccall "RegisterClassExA" registerClassExA :: Ptr WNDCLASSEXA -> IO ATOM
```

`registerClassExA` としているところはHaskell側から見える識別子なので、自由に指定できます。
`stdcall` じゃなくて `ccall` なの？と思われる方もいるかもしれませんが、x86_64の環境ではむしろ `stdcall` が指定できません。詳しい理由はちょっと調べてませんが多分これであってます。

#### 関数ポインタを使う

ウィンドウコールバックの指定で関数ポインタを渡す必要があります。その場合は、wrapperと名付けられた特殊な関数を `foreign import` で持ってきます。

```haskell
foreign import ccall "wrapper" makeWndProc :: (HWND -> CUInt -> WPARAM -> LPARAM -> IO LRESULT) -> IO WNDPROC
```

`WNDPROC` で具体的な返り値が隠れていますが、これは `FunPtr (HWND -> CUInt -> WPARAM -> LPARAM -> IO LRESULT)` としていますので、具体的な返り値は `IO (FunPtr (HWND -> CUInt -> WPARAM -> LPARAM -> IO HRESULT))` となります。
関数シグネチャの部分を適当な型変数 `a` とすると、wrapperのimportは以下のような形になります。

```haskell
foreign import ccall "wrapper" identifier :: a -> IO (FunPtr a)
```

任意の関数を受け取って、それを関数ポインタに変換しているだけ、というのが読み取れると思います。

#### 関数ポインタを呼ぶ

COMでは仮想関数テーブルから関数ポインタを得てそれを呼ぶ、といった場面が多く出てきます。関数ポインタはHaskell的には関数ではないため、なんとかして関数ポインタから呼べる関数を作り出す必要があります。
これは先述のwrapperと似たようなものをimportすることで実現できます。

```haskell
foreign import ccall "dynamic" dcallFunction :: FunPtr a -> a
```

関数ポインタ化がIOアクションなのに対して、こちらはIOではなく純粋関数となっています。

## HaskellでCOMインターフェイスをFFIして持ってくる

以上でほとんどの機能はFFIで持ってくることができます。

HaskellでCOMの機能を持ってくる場合は、C言語でのCOMインターフェイスを通して持ってきます。C++では特殊なクラスとして定義されていますが、C言語ならCOMインターフェイスはただの構造体になるため、ひたすら `Storable` インスタンスを定義することで持ってくることができます。

一つ例として、 `ID3D12CommandQueue` の定義の一部を出します。

```haskell:Windows.Direct3D12.CommandQueue
data ID3D12CommandQueueVtbl
data ID3D12CommandQueue = ID3D12CommandQueue (Ptr ID3D12CommandQueueVtbl)
instance ComInterface ID3D12CommandQueue where
  type VTable ID3D12CommandQueue = ID3D12CommandQueueVtbl
  guid _ = GUID 0x0ec870a6 0x5d7e 0x4c22 0x8cfc5baae07616ed
instance Storable ID3D12CommandQueue where
  sizeOf _ = 8
  alignment _ = 8
  peek p = ID3D12CommandQueue <$> peek (castPtr p)
  poke p (ID3D12CommandQueue vp) = castPtr p `poke` vp

_VTBL_INDEX_SIGNAL = 14
type PFN_Signal = Ptr ID3D12CommandQueue -> Ptr ID3D12Fence -> Word64 -> IO HRESULT
foreign import ccall "dynamic" dcall_signal :: FunPtr PFN_Signal -> PFN_Signal
signal :: Ptr ID3D12CommandQueue -> Ptr ID3D12Fence -> Word64 -> ComT IO ()
signal this fence value = liftIO (getFunctionPtr _VTBL_INDEX_SIGNAL this >>= \f -> dcall_signal f this fence value) >>= handleHRESULT
```

`ComT` とか `handleHRESULT` とかいろいろ見慣れない物がありますが、COMのエラーハンドリング用のMonad Transformer（実態は `ComT m a = ExceptT ComError m a`）と、 `HRESULT` から `ComT` を作り出すヘルパー関数です。

COMインターフェイスの構造は、大まかに以下のような感じになっています。

![COMStructPtr.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94115/a7ef76e4-57ae-26e3-956d-ab8a78555250.png)

オブジェクトの先頭に仮想関数テーブルへのポインタが格納されているため、それをオフセットつきでデリファレンスすることで該当メソッドにアクセスすることができます。仮想関数テーブルは、感覚としては関数ポインタの配列のようなものだと思っておけば大丈夫です。
上記例のなかで仮想関数テーブルにアクセスしているのは、 `getFunctionPtr _VTBL_INDEX_SIGNAL this` の箇所になります。 `getFunctionPtr` は出すまでもない気がしますが、仮想関数テーブルの先頭へのポインタを得て、さらにオフセット付きで `peek` している形になっています。これらの関数は大体どのインターフェイスでも同じコードになるので、 `ComInterface` のインスタンスをよしなに定義することで自動的に定義されるようにしています。

```haskell:Windows.ComBaseの一部
  vtable :: Ptr a -> IO (Ptr (VTable a))
  getFunctionPtr :: Int -> Ptr a -> IO (FunPtr f)
  vtable = peek . castPtr
  getFunctionPtr index this = vtable this >>= peek . flip plusPtr (index * 8)
```

`* 8` しているのは、対象アーキテクチャが64bitだからです。ちゃんとやるなら `sizeOf (undefined :: FunPtr a)` とかしてあげた方が良さそう

`_VTBL_INDEX_SIGNAL` は `14` であると定義されています。この `14` はどこからきたのかというと、C言語側の `struct ID3D12CommandQueueVtbl` における `Signal` の定義されている位置になっています。 `QueryInterface` を `0` として、目的の関数が構造体定義の何番目に出てくるかをひたすら目視で探します。