🐷

C# から C の DLL を呼ぶサンプル(SafeHandle 利用)

2022/02/10に公開

C# から C の DLL を呼ぶサンプル(SafeHandle 利用)

シナリオ

以下のようなシナリオでの典型的な実装例を示します。

  • C++ のクラスライブラリを C# で使いたい
  • C# からは C++ は使うのが困難なので C の DLL を呼ぶのが普通
  • そのため、 C++ クラスをラップした C の DLL を作成
  • その C の DLL を C# から呼び出したい

C# のプログラム
  └ C の DLL(Hoge_C.dll)
     └ C++ のクラスライブラリ

実装例

C# コード

Program.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

namespace CDllFromCSharp
{
    internal class Program
    {
        static void Main(string[] args)
        {
            using (HogeHoge hoge = new HogeHoge())
            {
                string inputFilePath = @"C:\TEMP\sample.hoge";

                var isOpened = hoge.open(inputFilePath);

                Debug.WriteLine("open : [" + inputFilePath + "] -> " + isOpened);

                if (isOpened)
                {
                    string saveFilePath = @"C:\TEMP\page_1.jpg";

                    var isSaveOk = hoge.saveAsImage(0, saveFilePath);

                    Debug.WriteLine("saveAsImage : [" + saveFilePath + "] -> " + isSaveOk);

                    hoge.close();
                }
                else
                {
                    Debug.WriteLine("open 失敗> " + inputFilePath);
                }
            }
        }
    }

    /// <summary>
    /// HogeLibrary ラップするクラス
    /// </summary>
    public class HogeHoge : IDisposable
    {
        private HogeHandle _handle;
        private bool _disposedValue = false;    // Dipsose パターン用

        public HogeHoge()
        {
            this._handle = HogeHandle.create();

            if (this._handle.IsInvalid)
            {
                throw new Win32Exception(Marshal.GetLastWin32Error(), "create");
            }
        }

        protected virtual void Dispose(bool disposing)
        {
            if (!this._disposedValue)
            {
                if (disposing)
                {
                    // マネージド状態を破棄します (マネージド オブジェクト)
                    // IDisposable を継承するものは マネージド オブジェクト です。

                    if (this._handle != null && !this._handle.IsInvalid)
                    {
                        // Free the handle
                        this._handle.Dispose();
                    }
                }

                // アンマネージド リソース (アンマネージド オブジェクト) を解放し、ファイナライザーをオーバーライドします
                // 大きなフィールドを null に設定します

                this._disposedValue = true;
            }
        }

        // 'Dispose(bool disposing)' にアンマネージド リソースを解放するコードが含まれる場合にのみ、ファイナライザーをオーバーライドします
        //~HogeHoge()
        //{
        //    // このコードを変更しないでください。クリーンアップ コードを 'Dispose(bool disposing)' メソッドに記述します
        //    Dispose(disposing: false);
        //}

        public void Dispose()
        {
            // このコードを変更しないでください。クリーンアップ コードを 'Dispose(bool disposing)' メソッドに記述します
            Dispose(disposing: true);
            GC.SuppressFinalize(this);
        }

        public bool open(string filePath_)
        {
            if (this._handle.IsInvalid)  // Is the handle disposed?
            {
                throw new ObjectDisposedException("handle was destroyed.");
            }

            bool ret = false;
            int openRet = HogeLibrary.hoge_open(this._handle, filePath_);

            if (0 == openRet)
            {
                ret = true;
            }

            return ret;
        }

        public void close()
        {
            if (this._handle.IsInvalid)  // Is the handle disposed?
            {
                throw new ObjectDisposedException("handle was destroyed.");
            }

            HogeLibrary.hoge_close(this._handle);
        }

        public bool isOpened()
        {
            if (this._handle.IsInvalid)  // Is the handle disposed?
            {
                throw new ObjectDisposedException("handle was destroyed.");
            }

            return HogeLibrary.hoge_isOpened(this._handle);
        }

        public bool saveAsImage(int pageIndex_, string saveFilePath_)
        {
            if (this._handle.IsInvalid)  // Is the handle disposed?
            {
                throw new ObjectDisposedException("handle was destroyed.");
            }

            bool ret = false;
            int saveRet = HogeLibrary.hoge_saveAsImage(this._handle, pageIndex_, saveFilePath_);

            if (0 == saveRet)
            {
                ret = true;
            }

            return ret;
        }
    }

    /// <summary>
    /// C の DLL API hoge_ 用の独自 unmanaged handle のラッパー
    /// unmanaged handle は SafeHandle でラップすることが推奨されています。
    /// cf. https://docs.microsoft.com/ja-jp/dotnet/standard/garbage-collection/implementing-dispose#implement-the-dispose-pattern-with-safe-handles
    /// </summary>
    internal sealed class HogeHandle : SafeHandle
    {
        public override bool IsInvalid => IntPtr.Zero == handle; // handle の値が 0 の場合、無効として扱う

        // Create a SafeHandle, informing the base class
        // that this SafeHandle instance "owns" the handle,
        // and therefore SafeHandle should call
        // our ReleaseHandle method when the SafeHandle
        // is no longer in use.
        private HogeHandle(IntPtr handle_) : base(IntPtr.Zero, ownsHandle: true)
        {
            this.SetHandle(handle_);
        }

        public static HogeHandle create()
        {
            return new HogeHandle(HogeLibrary.hoge_create());
        }

        protected override bool ReleaseHandle()
        {
	    // 呼び出し遷移は以下のようになります。
            // System.Runtime.InteropServices.SafeHandle.Finalize()
            // └ System.Runtime.InteropServices.SafeHandle.InternalFinalize()
            //    └ HogeHandle.ReleaseHandle()
	
            HogeLibrary.hoge_destory(this.handle);
            return true;
        }
    }

    /// <summary>
    /// C 言語 DLL 呼び出し用のクラス
    /// </summary>
    internal static class HogeLibrary
    {
        [DllImport("Hoge_C.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
        public extern static IntPtr hoge_create();

        [DllImport("Hoge_C.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
        public extern static void hoge_destory(IntPtr handle_);

        [DllImport("Hoge_C.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
        public extern static int hoge_open(HogeHandle handle_, string filePath_);

        [DllImport("Hoge_C.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
        public extern static void hoge_close(HogeHandle handle_);

        [DllImport("Hoge_C.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
        [return: MarshalAs(UnmanagedType.I1)]
        public extern static bool hoge_isOpened(HogeHandle handle_);

        [DllImport("Hoge_C.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
        public extern static int hoge_saveAsImage(HogeHandle handle_, int pageIndex_, string saveFilePath_);
    }
}

コードの説明

Program.Main

C の DLL を呼び出して動かすためのデモアプリ。

C# から使いやすいように作られた class HogeHoge の使い方を示します。

class HogeLibrary

Hoge_C.dll をラップするクラス。

ここでは C++ で Hoge クラスが実装されており、それをラップして C# で使えるように C の DLL 化したもの。C++ Hoge クラスインスタンスを new / delete するのが create / destory で、それ以外は Hoge クラスのメソッドを呼び出すラッパーです。
C の DLL でハンドルと言っているものの実体は C++ クラスのインスタンス(new Hoge)です。

【着目点】HogeHandle が使われていることに注目してください。

class HogeHandle : SafeHandle

HogeLibrary で利用される C でいうところの HANDLE (C# だと IntPtr)をラップするクラス。

C# だと unmanage の独自ハンドルは SafeHandle を継承したクラスを使うことが推奨されているため。

class HogeHoge : IDisposable

HogeLibrary をラップする C# のクラス。C# から C の DLL を呼びやすくするために、クラス化しています。

HogeHandle(マネージド オブジェクト) を内包しているので、Disposeパターンを用いています。

Discussion