前書き
この記事は、2023のUnityアドカレの12/9の記事です。
今年は、完走賞に挑戦してみたいと思います。Qiita君ぬい欲しい!
はじめに
C#でファイルを扱うときには、System.IO.FileStream
という抽象化された型で取り扱うことができます。C#…のランタイムである.NETは、これをOSレベルのファイルシステムにつなぐことで実際のファイルにアクセスできるということです。
Androidのネイティブ(NDKによるC++)とつなぐ必要があったり、このFileStreamが何者なのかを調べる必要がありました。本記事ではそれを追跡する過程をご紹介します。
とりあえずC#で書いてみる
適当にFileStreamを使ったC#コードを記述します。
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のコアライブラリ)の中にあるクラスです。
UnityのBCLのFile
UnityのBCLはMono実装のUnityカスタム版です。
var nativeHandle = MonoIO.Open (path, mode, access, share, options, out error);
this.safeHandle = new SafeFileHandle (nativeHandle, false);
MonoIOなるものをOpenし、そのハンドルをFileStream.safeHandle
に突っ込んでいます。つまり、FileStreamの裏にはMonoIOなるものがあるようです。まだ抽象層でした。MonoIO.Open
を追ってみます。
[MethodImplAttribute (MethodImplOptions.InternalCall)]
private unsafe extern static IntPtr Open (char* filename,
FileMode mode,
FileAccess access,
FileShare share,
FileOptions options,
out MonoIOError error);
InternalCall、つまり、BCLではなく、VM側に実装が移るということです。
UnityのVMのFile
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))
BCLのSystem.IO.MonoIO.Open
は、VMのves_icall_System_IO_MonoIO_Open
にBindingされていることがわかります。
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);
...
}
w32file系は、プラットフォームごとにファイルが分かれており、mono_w32file_create
も複数ありますが、今回はAndroidを狙うので、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);
}
次は、_wapi_open
です。
#include <fcntl.h>
static gint
_wapi_open (const gchar *pathname, gint flags, mode_t mode)
{
...
fd = open (pathname, flags, mode);
...
return(fd);
}
open
関数でどん詰まりになりました。これは、fcntl.h
に宣言されたPOSIX関数です。
ということで、UnityのAndroidにおける、FileStreamはPOSIXのopen関数で得られるファイルハンドルをラップしたものでした。
C#からファイルハンドルを取得する
FileStreamには、FileStream.SafeFileHandle
という尤もらしいプロパティがあります。
var nativeHandle = MonoIO.Open (path, mode, access, share, options, out error);
this.safeHandle = new SafeFileHandle (nativeHandle, false);
FileStream.Open
で、MonoIO.Open
の返り値をSafeFileHandleクラスに包んで控えていました。
MonoIO.Open
の返り値を遡って考えていきましょう
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);
ということで、
FileStream.SafeFileHandle = new SafeFileHandle(fileHandle_of_Posix);
であるということがわかりましたね。SafeFileHandleの中身はSafeFileHandle.handle
でIntPtr型として取りだすことができます。
まとめ
このように、C#のFileStreamが何者なのかがはっきりしました。例えばC#でFileStreamを開き、Androidのネイティブに渡したいときなどには、FileStream.SafeFileHandle.handle
を渡し、ネイティブの処理が終わったらCloseすればよいということです。(protectedなので適当な方法で取り出す、または、非推奨なHandleフィールドへアクセスする必要があります)