FileStreamの正体🤔（UnityAndroidの場合）

# 前書き

この記事は、2023のUnityアドカレの12/9の記事です。
今年は、完走賞に挑戦してみたいと思います。Qiita君ぬい欲しい！

https://qiita.com/advent-calendar/2023/present-calendar#qiita_award

# はじめに

C#でファイルを扱うときには、`System.IO.FileStream`という抽象化された型で取り扱うことができます。C#…のランタイムである.NETは、これをOSレベルのファイルシステムにつなぐことで実際のファイルにアクセスできるということです。

Androidのネイティブ（NDKによるC++）とつなぐ必要があったり、このFileStreamが何者なのかを調べる必要がありました。本記事ではそれを追跡する過程をご紹介します。

# とりあえずC#で書いてみる

適当にFileStreamを使ったC#コードを記述します。

```csharp
public class FileAccessTest : MonoBehaviour
{
    void Start()
    {
        var fs = new FileStream(
            Path.Combine(Application.persistentDataPath, "test.txt"),
            FileMode.Create);
        fs.Write(Encoding.UTF8.GetBytes("Hello World!"));
        fs.Dispose();
    }
}
```

FileStreamは、BCL（.NETのコアライブラリ）の中にあるクラスです。

https://learn.microsoft.com/ja-jp/dotnet/api/system.io.filestream?view=net-8.0

# UnityのBCLのFile

UnityのBCLはMono実装のUnityカスタム版です。

```csharp:mcs/class/corlib/System.IO/FileStream.cs
var nativeHandle = MonoIO.Open (path, mode, access, share, options, out error);
this.safeHandle = new SafeFileHandle (nativeHandle, false);
```

https://github.com/Unity-Technologies/mono/blob/2022.3.15f1/mcs/class/corlib/System.IO/FileStream.cs#L235-L242

MonoIOなるものをOpenし、そのハンドルを`FileStream.safeHandle`に突っ込んでいます。つまり、FileStreamの裏にはMonoIOなるものがあるようです。まだ抽象層でした。`MonoIO.Open`を追ってみます。

```csharp:mcs/class/corlib/System.IO/MonoIO.cs
[MethodImplAttribute (MethodImplOptions.InternalCall)]
private unsafe extern static IntPtr Open (char* filename,
    FileMode mode,
    FileAccess access,
    FileShare share,
    FileOptions options,
    out MonoIOError error);
```

https://github.com/Unity-Technologies/mono/blob/2022.3.15f1/mcs/class/corlib/System.IO/MonoIO.cs#L414-L420

InternalCall、つまり、BCLではなく、VM側に実装が移るということです。

# UnityのVMのFile

```c:mono/metadata/icall-def.h
ICALL_TYPE(MONOIO, "System.IO.MonoIO", MONOIO_39)
...
NOHANDLES(ICALL(MONOIO_16, "Open(char*,System.IO.FileMode,System.IO.FileAccess,System.IO.FileShare,System.IO.FileOptions,System.IO.MonoIOError&)", ves_icall_System_IO_MonoIO_Open))
```

https://github.com/Unity-Technologies/mono/blob/2022.3.15f1/mono/metadata/icall-def.h#L422C1-L446

BCLの`System.IO.MonoIO.Open`は、VMの`ves_icall_System_IO_MonoIO_Open`にBindingされていることがわかります。

```c:mono/metadata/w32file.c
HANDLE 
ves_icall_System_IO_MonoIO_Open (const gunichar2 *filename, gint32 mode,
				 gint32 access_mode, gint32 share, gint32 options,
				 gint32 *error)
{
    ...
    ret=mono_w32file_create (
        filename,
        convert_access ((MonoFileAccess)access_mode),
        convert_share ((MonoFileShare)share),
        convert_mode ((MonoFileMode)mode), attributes);
    ...
}
```

https://github.com/Unity-Technologies/mono/blob/2022.3.15f1/mono/metadata/w32file.c#LL581

w32file系は、プラットフォームごとにファイルが分かれており、`mono_w32file_create`も複数ありますが、今回はAndroidを狙うので、`w32file-unix.c`を見ます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280975/ce2c9eb9-e0c0-255b-dd58-84bf4472fc84.png)

```cpp:mono/metadata/w32file-unix.c
gpointer
mono_w32file_create(const gunichar2 *name, guint32 fileaccess, guint32 sharemode, guint32 createmode, guint32 attrs)
{
    ...
    fd = _wapi_open (filename, flags, perms);
    ...
    filehandle = file_data_create (type, fd);
    ...
    return GINT_TO_POINTER(((MonoFDHandle*) filehandle)->fd);
}
```

https://github.com/Unity-Technologies/mono/blob/2022.3.15f1/mono/metadata/w32file-unix.c#L1985-L2089C2

次は、`_wapi_open`です。

```c:mono/metadata/w32file-unix.c
#include <fcntl.h>

static gint
_wapi_open (const gchar *pathname, gint flags, mode_t mode)
{
    ...
    fd = open (pathname, flags, mode);
    ...
    return(fd);
}
```

https://github.com/Unity-Technologies/mono/blob/2022.3.15f1/mono/metadata/w32file-unix.c#L275-L314

`open`関数でどん詰まりになりました。これは、`fcntl.h`に宣言されたPOSIX関数です。

https://www.ibm.com/docs/ja/zos/3.1.0?topic=functions-open-open-file

ということで、UnityのAndroidにおける、FileStreamはPOSIXのopen関数で得られるファイルハンドルをラップしたものでした。

# C#からファイルハンドルを取得する

FileStreamには、`FileStream.SafeFileHandle`という尤もらしいプロパティがあります。

https://learn.microsoft.com/ja-jp/dotnet/api/system.io.filestream.safefilehandle?view=net-8.0

```csharp:mcs/class/corlib/System.IO/FileStream.cs
var nativeHandle = MonoIO.Open (path, mode, access, share, options, out error);
this.safeHandle = new SafeFileHandle (nativeHandle, false);
```

`FileStream.Open`で、`MonoIO.Open`の返り値をSafeFileHandleクラスに包んで控えていました。

https://github.com/Unity-Technologies/mono/blob/2022.3.15f1/mcs/class/corlib/System.IO/FileStream.cs#L235-L242

`MonoIO.Open`の返り値を遡って考えていきましょう

```csharp
FileStream.safeHandle = new SafeFileHandle(MonoIO.Open(...), false);
    => ves_icall_System_IO_MonoIO_Open (...);
    => mono_w32file_create(...);
    => GINT_TO_POINTER(((MonoFDHandle*) filehandle)->fd);
filehandle = file_data_create (type, fd)
filehandle->fd = fd
fd = _wapi_open(...)
    => open(pathname, flags, mode);
```

ということで、

```csharp
FileStream.SafeFileHandle = new SafeFileHandle(fileHandle_of_Posix);
```

であるということがわかりましたね。SafeFileHandleの中身は`SafeFileHandle.handle`でIntPtr型として取りだすことができます。

# まとめ

このように、C#のFileStreamが何者なのかがはっきりしました。例えばC#でFileStreamを開き、Androidのネイティブに渡したいときなどには、`FileStream.SafeFileHandle.handle`を渡し、ネイティブの処理が終わったらCloseすればよいということです。（protectedなので適当な方法で取り出す、または、[非推奨なHandleフィールド](https://learn.microsoft.com/ja-jp/dotnet/api/system.io.filestream.handle?view=net-8.0#system-io-filestream-handle)へアクセスする必要があります）