Google の Magika を Python から C#に移植するまでの過程 (Day 3 / 7)

Google のファイル判定プログラム Magika を Python から C# に移植する過程を共有する記事の第3回目です。

前回は C# で概念実証コードを書いて、オリジナルの Magika と同等の推論処理を C# でも実現できることを確認しました。いよいよ実際に Magika のコードを移植する作業を始めていきます。

すべての過程を書き連ねていくと長くなりすぎるので、工夫したところや詰まったポイントなどに絞って書いていきます。

## 目次
- [Day 1 : まずは Magika の中身を見てみよう](https://qiita.com/mkht/items/64b02b8648266d40380a)
- [Day 2 : C# で 概念実証コードを書いてみる](https://qiita.com/mkht/items/fa76b0238ed976d20ca9)
- [Day 3 : C# クラスライブラリとして Magika を移植していく](https://qiita.com/mkht/items/a7fa36c5446b5fad5092)
- [Day 4 : GitHub Copilot を使って作業効率アップ](https://qiita.com/mkht/items/2c2b23ceb0bc1df66599)
- [Day 5 : クラスライブラリとしての Magika を完成させる](https://qiita.com/mkht/items/9d7a8a16ed1d60e353fa)
- [Day 6 : コンソールアプリを作成する](https://qiita.com/mkht/items/0b2d1546cf8a25ab4cce)
- [Day 7 : 移植したMagikaをビルドし、動作確認する](https://qiita.com/mkht/items/f36d87afe50eae2be5f1)


## C# クラスライブラリとして Magika を移植していく

前回の概念実証コードはコンソールアプリケーションとして作成しましたが、最終的には Magika をライブラリとして PowerShell などから呼び出す形で使いたいので、C# のクラスライブラリとして移植していきます。

```cmd
dotnet new classlib --name magika
```

### ターゲットフレームワークの選択

まずターゲットフレームワークを決めないといけません。前回の概念実証コードはデフォルトの`net8.0`のまま作成しました。コンソールアプリならこれでも問題ないのですが、最終的に PowerShell から呼び出すことを考えると、慎重に選択しないといけません。

というのも PowerShell のバージョンによって実行可能なフレームワークが異なるからです。

#### PowerShell バージョンと実行可能な最大のフレームワーク
- PowerShell 7.4 : `net8.0`
- PowerShell 7.3 : `net7.0`
- PowerShell 7.2 : `net6.0`
- Windows PowerShell 5.1 : `netstandard2.0`

つまり`net8.0`だけでライブラリを作成すると、それを呼び出せるのは最新の PowerShell 7.4 に限られてしまいます。PowerShell 7.2 などの古いバージョンでは呼び出せません。

今回は Windows にデフォルトでインストールされている PowerShell 5.1 でも使えるようにしておきたかったので、`netstandard2.0`は必ず選択したいところです。

ただ、`netstandard2.0`はいささか古く、使える機能にどうしても制約が生じてしまいます。macOS や Linux などの環境でも動かすことを考えると、`netstandard2.0`では不十分な場合があります。そのため`net6.0`も入れることにします。Windows PowerShell 5.1 で使うときと、PowerShell 7.2 以上で使うときで、それぞれのバージョンに合わせて使い分けることになります。

クラスライブラリのプロジェクトを作成したら、プロジェクトファイル `magika.csproj` を編集します。

```xml
<TargetFramework>net8.0</TargetFramework>
```

この行を消して、次のように書き換えます。

```xml
<TargetFrameworks>netstandard2.0;net6.0;net8.0</TargetFrameworks>
```

### Python のコードを C# に移植していく

ここからは Python 版の Magika のソ－スコードを見ながら C# にひたすら移植していく作業になります。さいわい言語による文法の差異はあれど、基本的なプログラムの構造やアルゴリズムは共通しているので、それほど難しい作業ではありません。Python にも C# にも明るくない私ですが、Google 先生の助けを借りながら進めていきます。

#### Python と C# の型の対応例
| Python | C# |
| --- | --- |
| `None` | `null` |
| `str` | `string` |
| `bytes` | `byte[]` |
| `Optional[int]` | `int?` もしくは `Nullable<int>` |
| `List[int]` | `List<int>` |
| `Dict[str, Any]` | `Dictionary<string, object>` |

#### Python と C# のメソッドの対応例
| Python | C# |
| --- | --- |
| `len()` | `Array.Length` や `List<T>.Count` |
| `map()` | `Enumerable.Select()` |
| `path.is_file()` | `File.Exists(path)` |

C# に Python と同等のメソッドがない場合は、同等の処理になるようほかの方法で代替したり、独自のメソッドを作ったりする必要がある場合もあります。例えば Python の `bytes.lstrip()` は C# には直接対応するメソッドがないので、`Enumerable.SkipWhile()` を使って同等の処理を実現することになります。

```python
beg_full = beg_full.lstrip()
```

```csharp
beg_full = beg_full.SkipWhile(b => b is 32 or 9 or 10 or 11 or 12 or 13).ToArray();
```

クラス定義なども C# で書き直していきます。例えば Python 版の`types.py`で定義されている`ModelFeatures`クラスには`@dataclass`というデコレータが付いていて、調べた感じ C# でそれに対応するのはレコード型なのかな？という気がしたのでそのように書き直しました。

```python
# Python
@dataclass
class ModelFeatures:
    beg: List[int]
    mid: List[int]
    end: List[int]
```

```csharp
// C#
public record ModelFeatures(
    List<int> beg,
    List<int> mid,
    List<int> end
);
```

### 変数名やメソッド名の命名規則どうする問題

Python の Magika のコードを見ながら C# への移植作業をはじめてすぐに直面したのが、変数名やメソッド名の命名規則をどうするかという問題でした。

Python では一般に変数名やメソッド名はスネークケース（`snake_case`）が使われます。Magika もそのように書かれています。

```python
def _extract_features_from_bytes(
    self,
    content: bytes,
    beg_size: Optional[int] = None,
    mid_size: Optional[int] = None,
    end_size: Optional[int] = None,
) -> ModelFeatures:
    if beg_size is None:
        beg_size = self._input_sizes["beg"]
...
```

ところが C# ではキャメルケース（`CamelCase`）が一般的です。先のメソッド`_extract_features_from_bytes`を書き換えるならこうですね。

```csharp
ModelFeatures ExtractFeaturesFromBytes(
    byte[] content,
    int? begSize = null,
    int? midSize = null,
    int? endSize = null
)
{
    if(begSize == null)
    {
        begSize = InputSizes["beg"];
    }
...
```

さて、今回 Mgika を C# に移植するにあたり、命名規則の選択肢は2つあります。

- ベタ移植なんだから命名規則もオリジナルのままに、Python 流で実装する
- C# の流儀にあわせて命名規則を変える

どうしよう...と悩みつつ、最初は Python 流のまま進めていました。そのほうがコピペで移植していくのが楽だったのと、メンバ変数（プロパティ）がオリジナル版と揃っていたほうが使いやすいのではないかとも考えました。

しかし、最終的にはメソッド名は C# 流のアッパーキャメルケースに変えつつ、メンバ変数はオリジナル版と同じままにする折衷案を選ぶことにしました。これが良い選択なのかどうかはわかりませんが、とりあえず。


### 外部モデルの読み込み機能はオミットする

オリジナルの Magika ではファイル判定に使う機械学習モデルについて、パスを指定して外部から独自のモデルを読み込む機能がありますが、これは今回の移植作業ではオミットすることにしました。

というのも、この機能には機械学習モデルと一緒にその動作を定義する JSON ファイルを読み込む必要があるのですが、C# で外部の JSON ファイルを読み込む処理を書くのが思いのほか面倒に感じました。

Python なら`json.loads()`で一発なのに、C# では事前にデータモデルとなるクラスを定義してから`System.Text.Json.JsonSerializer.Deserialize<T>()`を使うのがお作法のようです。

一応`System.Text.Json.Nodes.JsonNode`などを使って汎用の JSON をパースする方法もあるようで、実際試したりしてみたのですが、どうもスッキリとしたコードにならない感じが拭えませんでした。

https://qreat.tech/3292/

そもそも、Magika をツールとして使用する立場で、独自のモデルを作成してそれを読み込ませたいというシチュエーションはあまりないのではないかとも思い、今回はこの機能を丸ごとオミットすることにしました。

> オリジナル版 Magika で独自モデルを指定する付近のコード  
> モデルファイルだけではなく関連する JSON ファイルも読み込む必要がある
```python
if model_dir is not None:
    self._model_dir = model_dir
else:
    # use default model
    self._model_dir = (
        Path(__file__).parent / "models" / self._default_model_name
    )

self._model_path = self._model_dir / "model.onnx"
self._model_config_path = self._model_dir / "model_config.json"
self._thresholds_path = self._model_dir / "thresholds.json"
self._model_output_overwrite_map_path = (
    self._model_dir / "model_output_overwrite_map.json"
)
```

### 外部ファイルは全て C# コードとして埋め込む

上記の外部モデル読み込み機能にも関連して、Python 版の Magika は動作パラメータ情報やファイル形式の定義などを JSON ファイルで用意して、これを実行時に読み込んで利用している箇所があります。`/python/magika/config/`や`/python/models/standard_v1/`などのディレクトリを見るといくつかの JSON ファイルがあるのがわかります。

今回 C# に移植するにあたり、思い切ってこれらの JSON ファイルは全て C# のクラスや構造体に変換して埋め込んでしまうことにしました。これにより面倒な JSON ファイルの読み込み処理を排除することができ、処理の簡素化や動作の高速化が期待できますし、JSON ファイルをライブラリと一緒に配布する必要もなくなりました。

例えば`/python/magika/config/magika_config.json`の内容を、

```json
{
    "default_model_name": "standard_v1",
    "medium_confidence_threshold": 0.5,
    "min_file_size_for_dl": 16,
    "padding_token": 256
}
```

C# で次のように構造体`MagikaConfig`として定義し直しています。

```csharp
public readonly record struct MagikaConfig
{
    public const string default_model_name = "standard_v1";
    public const float medium_confidence_threshold = 0.5f;
    public const int min_file_size_for_dl = 16;
    public const int padding_token = 256;
}
```

## 続く
作業的にはそこまで難しくはないものの、私が Python にも C# にも明るくないため一つ一つ調べながら、試しながらという感じでどうしても時間がかかります。[次回](https://qiita.com/mkht/items/2c2b23ceb0bc1df66599)も引き続き Magika の移植作業を進めていきます。