【C#.NET解説】unsafeコンテキストの使い方と注意点を徹底解説 メモリ操作を安全に扱う秘訣

2024年12月18日水曜日

API 基本 制御フロー

t f B! P L

名前空間 System

unsafeコンテキスト

自己紹介

おお、私は天と地を司る者だ。私が「unsafe」コンテキストを語る理由は、その力が天候や嵐と似ているからだ。コントロールすれば非常に便利だが、誤れば破壊的な結果を招く。安全なC#.NETの世界でありながら、この「unsafe」は名前の通り、安全の枠を外れた機能だ。

「unsafe」コンテキストを使うとポインタを操作できるが、その際には注意深く扱う必要がある。さて、どうやって使うのか、見てみよう。

基本機能

「unsafe」コンテキストは、C#でポインタ操作や低レベルメモリ操作を可能にする特別な機能だ。通常のC#プログラムではメモリ操作は抽象化されているが、「unsafe」を使うことで直接的なアクセスが可能になる。

次の例では、「unsafe」コンテキストの基本的な使い方を示している。

C#
unsafe
{
    int num = 42;
    int* pNum = # // numのアドレスを取得し、ポインタに格納
    Console.WriteLine($"ポインタが指す値: {*pNum}"); // ポインタの指す値を取得
}

このコードでは、変数のアドレスを取得し、それをポインタとして操作している。ただし、「unsafe」を使うには、プロジェクトで「Allow unsafe code(アンセーフコードの許可)」を有効にする必要がある。

.NET8での設定
.NET8での設定
.NET Frameworkの設定
.NET Frameworkの設定

よく使う場面と注意点

「unsafe」コンテキストは、以下のような状況で使われることが多い:

  • 低レベルAPIとの連携(例:ネイティブコードやハードウェアアクセス)
  • パフォーマンスが重要な場面での直接メモリ操作
  • 既存のCまたはC++ライブラリとの統合

注意点としては、メモリを直接操作するため、不適切なコードは重大なバグやセキュリティリスクにつながる可能性がある。

以下に「unsafe」を用いた場合の速度差を示している。実際の環境で試してほしい。

C# unsafeなし
using System;
using System.Diagnostics;

class Program
{
    static void Main()
    {
        int[] array = new int[100000000];
        Stopwatch sw = new Stopwatch();

        // 配列の初期化
        for (int i = 0; i < array.Length; i++)
        {
            array[i] = i;
        }

        sw.Start();
        ProcessArray(array);
        sw.Stop();
        Console.WriteLine($"Safe method took: {sw.ElapsedMilliseconds} ms");
    }

    static void ProcessArray(int[] array)
    {
        for (int i = 0; i < array.Length; i++)
        {
            array[i] *= 2;
        }
    }
}
C# unsafeあり
using System;
using System.Diagnostics;

class Program
{
    static void Main()
    {
        int[] array = new int[100000000];
        Stopwatch sw = new Stopwatch();

        // 配列の初期化
        for (int i = 0; i < array.Length; i++)
        {
            array[i] = i;
        }

        sw.Start();
        ProcessArrayUnsafe(array);
        sw.Stop();
        Console.WriteLine($"Unsafe method took: {sw.ElapsedMilliseconds} ms");
    }

    unsafe static void ProcessArrayUnsafe(int[] array)
    {
        fixed (int* p = array)
        {
            for (int i = 0; i < array.Length; i++)
            {
                p[i] *= 2;
            }
        }
    }
}

実例を用いた方法

ビットマップのピクセルデータを反転する処理を示す。

C#
using System;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;

class Program
{
    static void Main()
    {
        string filePath = @"C:\test.bmp";
        string outputFilePath = @"C:\test_processed.bmp";
        Stopwatch sw = new Stopwatch();

        sw.Start();
        ProcessAndSaveBitmap(filePath, outputFilePath);
        sw.Stop();
        Console.WriteLine($"Unsafe method took: {sw.ElapsedMilliseconds} ms");
    }

    unsafe static void ProcessAndSaveBitmap(string filePath, string outputFilePath)
    {
        using (Bitmap bmp = new Bitmap(filePath))
        {
            BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadWrite, bmp.PixelFormat);
            int bytesPerPixel = Image.GetPixelFormatSize(bmp.PixelFormat) / 8;
            int heightInPixels = bmpData.Height;
            int widthInBytes = bmpData.Width * bytesPerPixel;
            byte* PtrFirstPixel = (byte*)bmpData.Scan0;

            for (int y = 0; y < heightInPixels; y++)
            {
                byte* currentLine = PtrFirstPixel + (y * bmpData.Stride);
                for (int x = 0; x < widthInBytes; x += bytesPerPixel)
                {
                    // 反転処理
                    currentLine[x] = (byte)(255 - currentLine[x]);       // Blue
                    currentLine[x + 1] = (byte)(255 - currentLine[x + 1]); // Green
                    currentLine[x + 2] = (byte)(255 - currentLine[x + 2]); // Red
                }
            }
            bmp.UnlockBits(bmpData);

            // 画像を保存
            bmp.Save(outputFilePath, ImageFormat.Bmp);
        }
    }
}

具体的な実用例 Win32APIとの連携

次に、Win32APIを利用した「unsafe」の実用例を示す。ここでは、ポインタを使ってメモリコピーを行う。

C#
using System;
using System.Runtime.InteropServices;

class Program
{
    [DllImport("kernel32.dll", EntryPoint = "CopyMemory", SetLastError = false)]
    public static extern unsafe void CopyMemory(void* dest, void* src, uint count);

    static unsafe void Main()
    {
        int[] source = { 1, 2, 3, 4 };
        int[] destination = new int[4];

        fixed (int* pSource = source, pDestination = destination)
        {
            CopyMemory(pDestination, pSource, (uint)(source.Length * sizeof(int)));
        }

        Console.WriteLine("コピー後の配列: " + string.Join(", ", destination));
    }
}

このコードでは、ネイティブAPIを使って配列のメモリをコピーしている。

まとめとそのほかのメソッド

「unsafe」コンテキストは強力な機能を提供するが、誤用すると危険だ。これを適切に使うためには、以下の点を心がけよう:

  • プロジェクト設定で「Allow unsafe code」を有効にする。
  • nullポインタやメモリリークに注意する。
  • 必要な場合にのみ使用し、他の方法がないかを検討する。

このブログを検索

QooQ