2018/02/17

Buffer.MemoryCopyで任意の位置に書き込む方法

前回、アプリケーションドメインの境界を跨いで共有メモリにアクセスする方法を記載したが、書き込んでいた位置が常にメモリ領域の先頭だった。例で挙げたプログラムだと、メモリ領域の任意の位置に書き込もうと思うとちょっと考えなければならないことが生じるから、一応対応方法を記録しておく。

まず、データのコピーで使っているBuffer.MemoryCopyだが、これが癖があって使いにくい。

大体にして、F#でVoid*が何を意味するのか、それをどうやって得るのか説明がない。

ということで、それを実現したコードを示す。

前回同様、マネージドな配列から共有メモリへのコピーを題材にしている。

open System
open System.IO.MemoryMappedFiles
open Microsoft.FSharp.NativeInterop
open Microsoft.Win32.SafeHandles

[<EntryPoint>]
let main argv = 

    // 共有メモリ
    let mmapFile : MemoryMappedFile =
        MemoryMappedFile.CreateOrOpen( "abc", 8192L );

    // 共有メモリのハンドルを得る
    let handle : SafeMemoryMappedViewHandle =
        mmapFile.CreateViewAccessor().SafeMemoryMappedViewHandle

    // いったんNULL値を保持するポインタを作る(nativeptr<byte>型)
    let mutable p : nativeptr<byte> =
        NativePtr.ofNativeInt( nativeint 0 )

    // メモリブロックのアドレスを取得する
    handle.AcquirePointer( &p )

    // 型を変える
    let destAddress =                // nativeptr<unit>型(型宣言が書けない)
        (
            NativePtr.toNativeInt p  // nativeptr<byte>型をnativeint型に変換する
        ).ToPointer()                // nativeptr<unit>型にする

    // マネージドな配列
    let sourceArray : byte[] =
        Array.zeroCreate( 8192 );

    // コピー元領域のアドレス値を得る
    let sourceAddress =
        (
            NativePtr.toNativeInt(
                &&sourceArray.[0]   // 配列の先頭要素のアドレス(nativeptr<byte>型)を得る
            )                       // nativeint型に変換する
        ).ToPointer()               // nativeptr<unit>型にする

    // コピー元とコピー先を指定して、コピーする
    Buffer.MemoryCopy(
        sourceAddress,
        destAddress,
        8192L,
        8192L
    )

    // 共有メモリを解放できるようにする
    handle.ReleasePointer()

    0

まずは共有メモリの場合から説明する。

共有メモリ領域の先頭アドレスは、SafeMemoryMappedViewHandleクラスAcquirePointerメソッドで取得できる。引数として、NULLで初期化されたnativeptr<byte>型変数のアドレスを指定する必要がある。

nativeptr<byte>型の値は普通に定義できる。

let mutable p : nativeptr<byte> = ……

NULLで初期化するためには、数値の0をnativeptr<byte>型にキャストする必要があるが、それにはNativePtrモジュールofNativeIntメソッドを用いる。こいつの引数はnativeint型であるが、これはSystem.IntPtr型の別名で、単にアドレス幅と同じビット長を持つ整数だと言っているに過ぎない。

だから、nativeint 0をNativePtr.ofNativeIntでnativeptr<byte>にしてやればよいということになるので、こうなる。

let mutable p : nativeptr<byte> = NativePtr.ofNativeInt( nativeint 0 )

このNativePtr.ofNativeIntメソッドは便利で、好きなnativeint型の整数を、任意の型のネイティブポインタに変換することができる。

ここまでで、NULL値を保持するnativeptr<byte>型の値が得られた。後はAcquirePointerメソッドを呼んであればよいのだが、こいつの引数がbyrefになっているので、引数の前に&を付けてやる必要がある。なおかつ、この時に指定する変数はmutableでなければならない。

handle.AcquirePointer( &p )

ここで、pは上記の通りnativeptr<byte>型である。しかし、本当に欲しいものは、MSDNにはVoid*型とか、nativeptr<void>と記述されている型であり、Visual Studioの画面ではnativeptr<unit>と表記される、得体のしれないデータ型である。



これを得るためには、まず一度NativePtrモジュールtoNativeIntメソッドを用いて、nativeptr<byte>型をnativeint型に変換してやる。

そこからIntPtr.ToPointerメソッド を使って目的のVoid*だかnativeptr<void>だかnativeptr<unit>だかという型の値を得る。

それをまとめると下記になる。

let destAddress =
    ( NativePtr.toNativeInt p ).ToPointer()

上記のdestAddressがVoid*(またはnativeptr<void>とかnativeptr<unit>)であるため、このままBuffer.MemoryCopyメソッドの引数に指定できる。


では次に、マネージドな領域に対するコピーを考える。

これはさっきよりも簡単だ。F#の&&演算子(単項の演算子だ)を使うと、任意の型のnativeptrが得られる。後は、上記と同じ議論でnativeptrをNativePtr.toNativeIntメソッドによりnativeint型にして、ToPointerメソッド で目的の型の値が得られる。

let sourceAddress =
    ( NativePtr.toNativeInt( &&sourceArray.[0] ) ).ToPointer()


これでようやく、Buffer.MemoryCopyメソッドを呼ぶことができる。


では、タイトルにある通り、アドレスの任意の場所にアクセスするにはどうすればよいか。

大きくいって、方法は2つある。

1つはNativePtr.addを用いる方法で、もう1つはnativeint型で計算してしまう方法だ。

NativePtr.addは型の指定があり、指定された型のバイト幅を考慮してアドレス値を加算してくれる。これを使うのなら、下記のようになる。

let mutable p : nativeptr<byte> = 
    NativePtr.ofNativeInt( nativeint 0 )

handle.AcquirePointer( &p )

// アクセスする先を決定する
let p2 = NativePtr.add p 4096

// 型を変える
let destAddress =
    ( NativePtr.toNativeInt p2 ).ToPointer()

上記ではpはnativeptr<byte>型であり、たまたまbyteが1バイトであるため、4096大きいアドレス値が得られる。


nativeintで直に値を加算するのならこうなる。

let mutable p : nativeptr<byte> = 
    NativePtr.ofNativeInt( nativeint 0 )

handle.AcquirePointer( &p )

// 型を変える
let destAddress =
    (
        ( NativePtr.toNativeInt p ) + nativeint 4096
     ).ToPointer()

結局、nativeint型はただの整数なのだから、四則演算は自由にできる。ならば、バイト数を自前で計算して演算を行うのも自由にできる。

C言語での配列やポインタの計算よりもさらに野蛮になっているが、もはやネイティブなアドレスを直接操作する以上、マネージ言語の精神も型の安全性も全て踏みにじっているのだから、今更取り繕ったところで始まらない。正直言って、どっちでもいいんじゃないかと思う。


あ、あと、Void*とかnativeptr<void>とかnativeptr<unit>と言われる型の値は四則演算ができないし、NativePtr.addを使っても計算できない。だから、アドレス値の計算を行う場合はVoid*に変換する前に事を済ませなければならない。

0 件のコメント: