本体のプログラムがchar*型を要求するAPIへと繋がるC#の在り方

  • 概要

    C#側から、C++のchar *が引数となっている関数を利用するのであれば、
    StringBuilderなどを利用すればおわりです。

    いわゆる、C#がC言語APIを呼び出すパターンです。

    しかし、世の中逆の場合もあるのです。
    即ち、同じchar *でもC言語がC#のAPI経由で要求するパターンです。

    古いレガシーなプラグインシステムなどにおいて、
    C言語で作成することを前提としたAPIインターフェイスを
    C#で基本部分を実装しようとすると、こういった事態になることがあります。

    C++/CLIでやれば一瞬で解決する

    上記のような状況で、なおかつ.NETを利用したい場合は、C++/CLIで実装すればすぐ終わるのですが、
    今回はC#で突き進む場合の話となります。

    C#がchar *を返すことを求められることの辛さ

    大抵のことはC++よりC#の方がエレガントに達成できますが、いくつかC#の方が苦手なこともあります。
    その多くは今となっては利用するべきではないような古い考え方のプログラムであることが多いのですが、
    C#でエレガントな解決方法があまり浸透していないものの1つが以下のようなシチュエーションに対するプログラムです。

    C++/CLIソース
    __declspec(dllexport) char *GetStaticStrPtr() {
        // staticな文字列が確保され、そのアドレスは一度確保されると、
        // pin_ptrがたったものと同様で勝手に移動したり、といったことはない。
        // 「テストテスト」確保されたものの解放は、このdllの解放に合わせて自動。
        return "テストテスト"; 
    }
    

    上記のような

    • fixedなポインタを求められ、dllが終了するまで確保しつづける
    • 呼び出し元の本体プログラムは GetStaticStrPtr によって得た「ポインタが指す文字列の内容」を複製したりはせず
      得られたポインタが指す先をそのまま利用。
      GetStaticStrPtr が呼び出された後も、確保した文字列はそのままdll側が保持
    • 解放は呼び出し元がやるのではなく、dll側がやる。

    といった形となってしまっている場合もあり、
    非常に動作は高速なのですが、
    このような形のインターフェイスへと繋げていける汎用的なC#の型を作成するのは、
    簡単そうでいて、そういうわけにもいかず、ひと工夫が必要です。

    C# StaticStrPtrHandle (Ansi版)
    using System;
    using System.Runtime.ConstrainedExecution;
    using System.Runtime.InteropServices;
    using System.Security.Permissions;
    
    [SecurityPermission(SecurityAction.Demand, UnmanagedCode = true)]
    public sealed class StaticStrPtrHandle : SafeHandle
    {
        private StaticStrPtrHandle() : base(IntPtr.Zero, true) { }
        public StaticStrPtrHandle(String managedString) : base(IntPtr.Zero, true)
        {
            handle = Marshal.StringToHGlobalAnsi(managedString);
        }
    
        // StringをStaticStrPtrに暗黙キャストした際の挙動
        // implicitではなくexplicitにして明示キャストの方がよいかも
        public static implicit operator StaticStrPtrHandle(String managedString)
        {
            return new StaticStrPtrHandle(managedString);
        }
    
        // StaticStrPtrをStringに暗黙キャスト可能とする
        public static implicit operator String(StaticStrPtrHandle managedString)
        {
            return managedString.ToString();
        }
    
        // SafeHandleでは上書き必須
        [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
        protected override bool ReleaseHandle()
        {
            try
            {
                Marshal.FreeHGlobal(handle);
                this.handle = IntPtr.Zero;
            }
            catch (Exception ex)
            {
                System.Diagnostics.Trace.WriteLine("文字列解放エラー:\n" + ex.Message);
                return false;
            }
    
            return true;
        }
    
        // SafeHandleでは上書き必須
        public override bool IsInvalid
        {
            get { return (IntPtr.Zero == handle); }
        }
    
        // String型的な振る舞いをするようにしておいた方がなにかと便利
        public override string ToString()
        {
            if (this.IsInvalid)
            {
                return String.Empty;
            }
            else
            {
                return Marshal.PtrToStringAnsi(handle);
            }
        }
    }
    
    C# StaticStrPtrHandle (Unicode版)
    using System;
    using System.Runtime.ConstrainedExecution;
    using System.Runtime.InteropServices;
    using System.Security.Permissions;
    
    [SecurityPermission(SecurityAction.Demand, UnmanagedCode = true)]
    internal sealed class StaticWStrPtrHandle : SafeHandle
    {
        private StaticWStrPtrHandle() : base(IntPtr.Zero, true) { }
        public StaticWStrPtrHandle(String managedString) : base(IntPtr.Zero, true)
        {
            handle = Marshal.StringToHGlobalUni(managedString);
        }
    
        public static implicit operator StaticWStrPtrHandle(String managedString)
        {
            return new StaticWStrPtrHandle(managedString);
        }
    
        public static implicit operator String(StaticWStrPtrHandle managedString)
        {
            return managedString.ToString();
        }
    
        [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
        protected override bool ReleaseHandle()
        {
            try
            {
                Marshal.FreeHGlobal(handle);
            }
            catch (Exception ex)
            {
                System.Diagnostics.Trace.WriteLine("文字列解放エラー:\n" + ex.Message);
                return false;
            }
    
            return true;
        }
    
        public override bool IsInvalid
        {
            get { return (IntPtr.Zero == handle); }
        }
    
        public override string ToString()
        {
            if (this.IsInvalid)
            {
                return String.Empty;
            }
            else
            {
                return Marshal.PtrToStringUni(handle);
            }
        }
    
    }
    
    使う
    class Program
    {
        static StaticStrPtrHandle hStrPtr = "テストテスト";
        static IntPtr GetStrPtr()
        {
            return hStrPtr.DangerousGetHandle();
        }
    }
    

    StaticStrPtrHandleのようなものであれば、
    String型っぽく利用しつつも、返り値として得たIntPtr値は、
    (ToPointer 等を介して)、char *としてC言語でそのまま利用可能です。
    ポインタが指し示していた先の内容(C#の.dllで確保されていた)ハズのものが途中でガベージコレクトで移動してしまい存在しない、
    といった事態を回避できます。