構造体配列のメモリブロックコピー

C言語では memcpy(); というメモリをブロックコピーするメソッドがあります。また WindowsAPI では CopyMemory(); がそれにあたります。

C# ではメモリ保護の仕組みが厳しく、通常ではそういった作業をすることはできません。ただし、ユーザの責任において unsafe を明示し、その中でメモリ操作系の WindowsAPI をコールすることは許可されています。

ここでは 24bits パックの画像データを想定して、メモリ内容のブロックコピーを実施する方法を示します。

下記コードのコンパイルするためには、VisualStudio のビルドオプションで「アンセーフコードの許可」を実施してください。ビルドオプションの設定方法は下記の記事をごらんください。

アンセーフunsafeコードを許可する

データのコピーやポインタ操作などアンセーフ unsafe なコードのビルドを実施する方法を紹介します。

ソースコードを解説します。

42~48行目、LayoutKind.Explicit と FieldOffset(n) でフィールド(メンバ)間がギッチリ詰まった構造体を定義します。ひとつのデータが1バイトなので構造体の1要素は3バイトになります。

つぎに Fig. 1 と Fig. 2 をごらんください。DataSrc、DataDst ともに構造体が4要素で構成された配列です。配列名に使っているサフィックスで Src は Source の略です。同様に Dst は Desitination の略です。それぞれ、コピー"元"、コピー"先"を意味します。

本記事のソースコードは、Fig. 1 の状態からメモリーコピーを実施して Fig. 2 の状態にします。

Fig. 1 メモリーコピー実行前
Fig. 2 メモリーコピー実行後

19~39行目、Windows のシステムライブラリファイルである Kernel32.dll にあるメモリーコピー用のメソッド CopyMemory() の定義です。SDK や MFC でプログラミングしたことがある人ならば、よく使ったと思います。

7個もオーバーロードを定義したのは引数の型に応じて引数をキャストするのが面倒だからです。いちいちキャストするのが面倒だと思わない場合は void* のタイプを1種類だけ定義するのでかまいません。

64、65行目、コピー元 Dst のデータ領域と、コピー先 Src のデータ領域を確保します。

69~78行目、ループでコピー元には 10, 11, 12, 20, 21... とデータを格納し、コピー先は 0 を格納しておきます。

84~93行目、メモリーコピーをする前の、DataSrc と DataDst に格納されている内容をダイアログボックスで視認するためにストリングビルダに追加します。デバッガで途中停止させて DataSrc と DataDst の内容を確認する場合は、このループブロックのコードは不要です。

99~115行目、ここはアンセーフブロックです。ここは危険なメモリ操作をやる場所であることをコンパイラに示しておきます。

102行目、fixed をつかって、このブロックにおいては DataSrc や DataDst の動的再配置が生じるのを禁止しておきます。メモリの動的再配置が禁止されているということは DataSrc と DataDst のゼロ要素目の先頭のメモリアドレスが変化しないということです。こうしておけば fixed ブロックの中で CopyMemory() が安全に使えるということになります。

112行目、たった1行の CopyMemory() というメソッドで連続したメモリの内容をコピーしています。

CopyMemory() は、第1引数がコピー先のゼロ要素目のメモリアドレス、第2引数がコピー元のゼロ要素目のメモリアドレス、第3引数がコピーするメモリバイト数です。最初のほうがコピー先であることに注意してください。なぜ不自然に最初がコピー先かというと、MASMアセンブラ記法の mov と rep movsb の影響を引きずっているからです。

第3引数は、構造体の要素数と sizeof() を使ってコピーするメモリバイト数を算出します。構造体の要素数ではないことに注意してください。

120~129行目、デバッガで内容を確認するならば不要です。やっていることは 84~93行目と同じです。

下記の名前空間の追加をお忘れなく.
using System.Runtime.InteropServices;

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

// この using 追加を忘れずに. Don't forget to add this sentence.
using System.Runtime.InteropServices;

namespace aaa
{
	public partial class Form1 : Form
	{

		// データをコピーする.
		[DllImport("Kernel32.Dll")] // オーバーロード 0.
		public static unsafe extern void CopyMemory( void* dst, void* src, int bytes );

		[DllImport("Kernel32.Dll")] // オーバーロード 1.
		public static unsafe extern void CopyMemory( IntPtr dst, IntPtr src, int bytes );

		[DllImport("Kernel32.Dll")] // オーバーロード 2.
		public static unsafe extern void CopyMemory( byte* dst, byte* src, int bytes );

		[DllImport("Kernel32.Dll")] // オーバーロード 3.
		public static unsafe extern void CopyMemory( byte* dst, IntPtr src, int bytes );

		[DllImport("Kernel32.Dll")] // オーバーロード 4.
		public static unsafe extern void CopyMemory( void* dst, IntPtr src, int bytes );

		[DllImport("Kernel32.Dll")] // オーバーロード 5.
		public static unsafe extern void CopyMemory( IntPtr dst, byte* src, int bytes );

		[DllImport("Kernel32.Dll")] // オーバーロード 6.
		public static unsafe extern void CopyMemory( IntPtr dst, void* src, int bytes );

		// 24ビットパックのデータ構造体の宣言.
		[StructLayout( LayoutKind.Explicit)]
		public struct GY_IMG_BPP24
		{
			[FieldOffset(0)] public byte B;
			[FieldOffset(1)] public byte G;
			[FieldOffset(2)] public byte R;
		}

		public Form1()
		{
			InitializeComponent();
		}

		private void button1_Click( object sender, EventArgs e )
		{

			StringBuilder sb = new StringBuilder();
			const String STRFMT_SRC = "Src[{0}] {1},{2},{3}";
			const String STRFMT_DST = "Dst[{0}] {1},{2},{3}";

			// 24ビットパックの画像データ領域を確保する.
			const int NUM_DATA = 4;
			GY_IMG_BPP24 [] DataSrc = new GY_IMG_BPP24[ NUM_DATA ];
			GY_IMG_BPP24 [] DataDst = new GY_IMG_BPP24[ NUM_DATA ];


			// ループで値を格納する.
			for ( int k = 0; k < NUM_DATA; k++ )
			{
				DataSrc[k].B = (byte)( (( k + 1 ) * 10 ) + 0 );
				DataSrc[k].G = (byte)( (( k + 1 ) * 10 ) + 1 );
				DataSrc[k].R = (byte)( (( k + 1 ) * 10 ) + 2 );

				DataDst[k].B = 0;
				DataDst[k].G = 0;
				DataDst[k].R = 0;
			}

			// デバッグ確認用文字列ビルダをクリアする.
			sb.Clear();

			// デバッグ確認用文字列ビルダに追加する.
			for ( int k = 0; k < NUM_DATA; k++ )
			{
				String str0 = String.Format( STRFMT_SRC, k, ( DataSrc[k].B ), ( DataSrc[k].G ), ( DataSrc[k].R ) );
				String str1 = String.Format( STRFMT_DST, k, ( DataDst[k].B ), ( DataDst[k].G ), ( DataDst[k].R ) );

				sb.AppendLine( str0 );
				sb.AppendLine( str1 );
				sb.AppendLine( "" ); // 空行.

			}

			// いったんメッセージボックスでデータの内容を示す.
			String strmsg_before = sb.ToString().Trim();
			MessageBox.Show( strmsg_before, "コピー前" );

			unsafe
			{
				// 構造体の配列の先頭のポインタを取得する.
				fixed( GY_IMG_BPP24* dst = &( DataDst[0] ), src = &( DataSrc[0] ) )
				{
					// byte型のポインタにむりやりキャストする.
					byte* pt_dst = (byte*)( dst );
					byte* pt_src = (byte*)( src );

					// コピーバイト数を計算する.
					int copy_bytes = NUM_DATA * sizeof( GY_IMG_BPP24 );

					// ブロックコピーを実施する.
					CopyMemory( pt_dst, pt_src, copy_bytes );

				}
			}

			// デバッグ確認用文字列ビルダをクリアする.
			sb.Clear();

			// デバッグ確認用文字列ビルダに追加する.
			for ( int k = 0; k < NUM_DATA; k++ )
			{
				String str0 = String.Format( STRFMT_SRC, k, ( DataSrc[k].B ), ( DataSrc[k].G ), ( DataSrc[k].R ) );
				String str1 = String.Format( STRFMT_DST, k, ( DataDst[k].B ), ( DataDst[k].G ), ( DataDst[k].R ) );

				sb.AppendLine( str0 );
				sb.AppendLine( str1 );
				sb.AppendLine( "" ); // 空行.
			}

			// メッセージボックスでコピー後のデータの内容を示す.
			String strmsg_after = sb.ToString().Trim();
			MessageBox.Show( strmsg_after, "コピー後" );

		}

	}

}

Fig. 3 と Fig. 4 が、112行目の CopyMemory() 実行前と実行後の DataSrc と DataDst に格納されている値を示したダイアログです。

Fig. 3 が97行目にあたります。Fig. 4 が133行目にあたります。

Fig. 3 メモリコピー実行前のダイアログ
Fig. 4 メモリコピー実行後のダイアログ

この記事で紹介したソースは kernel32.dll で定義されている CopyMemory のエントリポイントの明示をしておりません。.NET Framework のアプリケーションでは問題なく動きますが、.NET Core のアプリケーションでは CopyMemory をコールすると実行時例外が発生します。その場合は下記に示す記事を参考にして EntryPoint として "RtlCopyMemory" を明示してください。

.NET Framework で使っていた CopyMemory が .NET Core で使えなくなった場合の対処法

.NET Framework のアプリケーションで問題なかった CopyMemory が、.NET Core にすると実行時例外を出してしまう場合の対処法をお知らせします。