C/C++向けのdllをC#のアプリケーションから呼び出そう

## はじめに
はじめまして。リンクラフト株式会社のzackeyです。
リンクラフトアドベントカレンダーの12日目を担当します。
普段は工場のお客様向けの業務システム開発で要件定義から製造まで担当しています。
搬送用のロボットに命令を送信するようなファクトリーオートメーションや、
製品の寸法などの測定結果を収集してグラフで表示するような品質管理システムをやってまして、
早いものでもう6年ほど同じお客様向けのプロジェクトに従事しています。

### 工場は古いものばかり！
製造してるモノにもよると思うのですが、工場で使われている機械は古いものばかりです。
僕のお客様ですと鉄製品を刃物/砥石で削るような加工機からμm単位の寸法を測れる測定機など色々な機械を置いてます。
それぞれスタンドアローンで加工だったり測定ができるように、加工条件や測定条件が入力できるタッチパネルがついているんですが、OSがWindowsXPだったりします(笑)
通信のためにネットワークを構築してたりはするのですが、基本的に外部とのネットワークは遮断しているので、やはり機械自体が壊れでもしない限りなかなか交換しようとはなりませんよね～。
加工機高いですからね。オーダーメイドとかしようものなら数千万円するみたいです。

### そんな機械たちにアプリケーションからデータを送受信するとなると…？
実際にはそんな機械たちと直接データを送受信することはあまりないと思います！！笑
基本的に複数の加工機で順番に加工することを想定しているようなラインだと、[PLC](https://ja.wikipedia.org/wiki/%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9E%E3%83%96%E3%83%AB%E3%83%AD%E3%82%B8%E3%83%83%E3%82%AF%E3%82%B3%E3%83%B3%E3%83%88%E3%83%AD%E3%83%BC%E3%83%A9)を置いてることがほとんどです。
前の加工機で削った値を受けて、次の加工機に適切な加工条件を渡す…そんなプログラムをGUIで直観的に作れるのがPLCです。
アプリケーションからデータの送受信をするのも簡単で、PLC付属のライブラリでも、ソケット通信で電文をやり取りする形でもデータを送受信できます。こちらはC#用のライブラリも提供されていることでしょう。多分。かつて作ったラッパー使い続けてるので知りませんが。
でPLCに送った値をそのまま加工機に落とす/PLCが受け取った値を吸い上げるだけ。
PLC側のプログラムは詳しくないので加工機とどう接続しているのか分からんのですが、とりあえずPLC経由であればアプリケーション側はラクチンです。

### が・・・・・駄目っ・・・・・！
しかしながら全てPLC経由の環境を構築できるとは限らないもの。直接加工機やら測定機やらとデータを送受信しなければならないシチュエーションはやっぱりあります。
そんなときC/C++用のライブラリしか提供されていない！ってことがよくあります。古いからね。

## C言語向けのダイナミックリンクライブラリ(dll)を呼び出すラッパーをC#で作ろう
というわけで、C/C++用のライブラリを呼び出す処理を実装してみようと思います。
[System.Runtime.InteropServices.DllImport](https://learn.microsoft.com/ja-jp/dotnet/api/system.runtime.interopservices.dllimportattribute?view=net-7.0)を使用しましょう。
kernel32.dllを使用して、C/C++向けdll内の関数をムリヤリ呼び出す形になります。

### kernel32.dllを使用して別のdllを直接呼び出す基本処理の実装
まずはdllの関数を指定してCallする基本処理を実装します。以下のコードは使用したいライブラリに関わらず共通で使えるでしょう。

```C#
/// <summary>
/// アンマネージ DLL の遅延バインディングに関するクラス
/// </summary>
public class DllLoader : IDisposable
{
    private static class NativeMethods
    {
        /// <summary>
        /// 指定された実行可能モジュールを、呼び出し側プロセスのアドレス空間内にマップします。
        /// </summary>
        /// <param name="lpLibFileName">実行可能モジュールの名前を保持する null で終わる文字列へのポインタ</param>
        /// <returns>モジュールのハンドル</returns>
        [System.Runtime.InteropServices.DllImport("kernel32.dll", EntryPoint = "LoadLibrary", BestFitMapping = false, ThrowOnUnmappableChar = true)]
        public static extern System.IntPtr LoadLibrary([System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.LPStr)] System.String lpLibFileName);

        /// <summary>
        /// ダイナミックリンクライブラリ（DLL）が持つ、指定されたエクスポート済み関数のアドレスを取得します。
        /// </summary>
        /// <param name="hModule">希望の関数を保持する DLL モジュールのハンドル</param>
        /// <param name="lpProcName">関数名を保持する null で終わる文字列へのポインタ</param>
        /// <returns>DLL のエクスポート済み関数のアドレス</returns>
        [System.Runtime.InteropServices.DllImport("kernel32.dll", EntryPoint = "GetProcAddress", BestFitMapping = false, ThrowOnUnmappableChar = true)]
        public static extern System.IntPtr GetProcAddress(System.IntPtr hModule, [System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.LPStr)]string lpProcName);

        /// <summary>
        /// ロード済みのダイナミックリンクライブラリ（DLL）モジュールの参照カウントを 1 つ減らします。
        /// 参照カウントが 0 になると、モジュールは呼び出し側プロセスのアドレス空間からマップ解除され、そのモジュールのハンドルは無効になります。
        /// </summary>
        /// <param name="hModule">ロード済みの DLL モジュールのハンドル</param>
        /// <returns>関数が成功した場合は 0 以外</returns>
        [System.Runtime.InteropServices.DllImport("kernel32.dll", EntryPoint = "FreeLibrary")]
        [return: System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.Bool)]
        public static extern bool FreeLibrary(System.IntPtr hModule);
    }

    /// <summary>
    /// DLL のパスを取得、設定します。
    /// </summary>
    public string DllPath
    {
        get;
        set;
    }

    /// <summary>
    /// アンマネージ DLL が使用可能かどうかを取得、設定します。
    /// </summary>
    public bool IsAvailable
    {
        get;
        set;
    }

    /// <summary>
    /// リソースが解放されているかどうかを取得、設定します。
    /// </summary>
    protected bool Disposed
    {
        get;
        set;
    }

    /// <summary>
    /// DLL モジュールのハンドルを取得、設定します。
    /// </summary>
    private System.IntPtr ModuleHandle
    {
        get;
        set;
    }

    /// <summary>
    /// 関数名とその <see cref="System.Delegate"/> のコレクションを取得、設定します。
    /// </summary>
    private System.Collections.Generic.Dictionary<string, System.Delegate> Functions
    {
        get;
        set;
    }

    /// <summary>
    /// <see cref="MMFrame.Diagnostics.UnManagedDll"/> オブジェクトを生成します。
    /// </summary>
    public DllLoader()
    {
        Initialize();
    }

    /// <summary>
    /// <see cref="MMFrame.Diagnostics.UnManagedDll"/> オブジェクトを生成します。
    /// </summary>
    /// <exception cref="System.IO.FileNotFoundException"><paramref name="dllPath"/> に DLL が存在しない場合、スローされます。</exception>
    /// <param name="dllPath">DLL のパス</param>
    public DllLoader(string dllPath)
    {
        Initialize();
        this.Load(dllPath);
    }

    /// <summary>
    /// <see cref="MMFrame.Diagnostics.UnManagedDll"/> のデストラクタ
    /// </summary>
    ~DllLoader()
    {
        Dispose(false);
    }

    /// <summary>
    /// アンマネージ DLL を読み込みます。
    /// </summary>
    /// <exception cref="System.IO.FileNotFoundException"><paramref name="dllPath"/> に DLL が存在しない場合、スローされます。</exception>
    /// <param name="dllPath">DLL のパス</param>
    /// <returns>読み込みが成功した場合は true</returns>
    public bool Load(string dllPath)
    {
        this.DllPath = dllPath;

        if (!System.IO.File.Exists(this.DllPath))
        {
            throw new System.IO.FileNotFoundException(this.DllPath + " は存在しません。");
        }

        this.ModuleHandle = NativeMethods.LoadLibrary(this.DllPath);
        this.IsAvailable = true;

        return this.IsAvailable;
    }

    /// <summary>
    /// 指定されたエクスポート済み関数を取得します。
    /// </summary>
    /// <exception cref="System.IO.FileNotFoundException">DLL が存在しない場合、スローされます。</exception>
    /// <exception cref="System.NotImplementedException">DLL に指定された関数が存在しない場合、スローされます。</exception>
    /// <typeparam name="T">エクスポート済み関数の定義</typeparam>
    /// <returns>エクスポート済み関数</returns>
    public T GetProcAddress<T>() where T : class
    {
        if (!this.IsAvailable)
        {
            throw new System.IO.FileNotFoundException(this.DllPath + " は存在しません。");
        }

        string funcName = typeof(T).Name;

        if (!this.Functions.ContainsKey(funcName))
        {
            System.IntPtr procAddress = NativeMethods.GetProcAddress(this.ModuleHandle, funcName);

            if (procAddress == System.IntPtr.Zero || procAddress == null)
            {
                throw new System.NotImplementedException(this.DllPath + " に " + funcName + " は見つかりません。");
            }

            this.Functions[funcName] = System.Runtime.InteropServices.Marshal.GetDelegateForFunctionPointer(procAddress, typeof(T));
        }

        return this.Functions[funcName] as T;
    }

    /// <summary>
    /// 割り当てられたリソースを解放します。
    /// </summary>
    public void Dispose()
    {
        Dispose(true);
        System.GC.SuppressFinalize(this);
    }

    /// <summary>
    /// 割り当てられたリソースを解放します。
    /// </summary>
    /// <param name="disposing">マネージドリソースの解放をする場合は true</param>
    protected void Dispose(bool disposing)
    {
        if (this.Disposed)
        {
            return;
        }

        this.Disposed = true;

        if (disposing)
        {
            this.Functions = null;
        }

        if (this.ModuleHandle != System.IntPtr.Zero || this.ModuleHandle != null)
        {
            NativeMethods.FreeLibrary(this.ModuleHandle);
            this.ModuleHandle = System.IntPtr.Zero;
        }
    }

    /// <summary>
    /// 初期化します。
    /// </summary>
    private void Initialize()
    {
        this.IsAvailable = false;
        this.Disposed = false;
        this.Functions = new System.Collections.Generic.Dictionary<string, System.Delegate>();
        this.ModuleHandle = System.IntPtr.Zero;
    }
}
```
### DllLoaderを継承してdll内の処理を記述するクラス
そしてこっちが、使用したいdllに応じたクラスになってます。
例として、ある機器に接続しにいく処理だけ書いてみます。
ここでキモとなるのは以下のポイントかと思います。
1. dllの関数をdelegateで定義する(Connect_s)
1. 構造体を引数で渡す場合、構造体を定義する(CERTIFY)
1. 接続処理(Connect)で、dll内の関数(Connect_s)を呼び出す。

dllを直接呼び出すのでデータ型の指定はかなりシビアです。
ushortとshortは別物です。当たり前だけど。先頭1bitの意味が変わってしまう！
intなんて指定しようものなら16bitも長くメモリを読み取ってしまいます。
dllのリファレンスとにらめっこしながら設定しましょう。

```C#
public class ConnectionManager : DllLoader
{
    // DLLのパス
    private const string DLL_PATH = "XXXXXX.dll";

    // ハンドルキー
    private ushort _hndl;

    /// <summary>
    /// ConnectionManagerクラスのインスタンスを初期化します。
    /// </summary>
    public ConnectionManager() : base(DLL_PATH)
    {
    }

    /// <summary>
    /// 接続処理を呼び出します。
    /// </summary>
    /// <param name="ip">接続先IPを指定します。</param>
    /// <param name="port">接続先IPに対するポート番号を指定します。</param>
    /// <param name="appName">接続に使用するアプリケーション名を指定します。</param>
    /// <param name="userPasswd">接続に使用するユーザーパスワードを指定します。</param>
    /// <returns>エラーコード(0:正常)</returns>
    public int Connect(string ip, ushort port, string appName, string userPasswd)
    {
        var certify = new CERTIFY();
        certify.vendorCode = XXXXXX.VENDER_CODE;
        certify.appName = appName;
        certify.userPasswd = userPasswd;

        return GetProcAddress<Connect_s>()(out _hndl, ip, port, certify);
    }

    /// <summary>
    /// 接続処理の関数への参照を定義します。
    /// </summary>
    /// <param name="hndl">ライブラリハンドルを格納する変数へのポインタです。</param>
    /// <param name="ipaddress">接続先IPを指定します。</param>
    /// <param name="port">接続先IPに対するポート番号を指定します。</param>
    /// <param name="certify">接続に使用する認証情報を設定します。</param>
    /// <returns>エラーコード(0:正常)</returns>
    private delegate int Connect_s(out ushort hndl, string ipaddress, ushort port, [MarshalAs(UnmanagedType.Struct)] CERTIFY certify);

    /// <summary>
    /// 認証情報を格納します。
    /// </summary>
    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
    private struct CERTIFY
    {
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 33)]
        public string vendorCode;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 7)]
        public string dummy;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 33)]
        public string appName;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 7)]
        public string dummy2;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 33)]
        public string userPasswd;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 15)]
        public string dummy3;
    }
}
```

### 接続！
あとは他のクラスでConnectionManagerクラスのインスタンスを生成してConnectメソッドを実行すればC/C++のdll内のConnect_s関数が呼ばれてオールドタイプな機械と接続することができるでしょう。

## 最後に
今回は自分が実装で以前マジかーと思いながら製造した部分を振り返ってみました。
工場はAIを生かした自動化など積極的に色々なシステムを導入している現場です。
まだまだ製造図が紙ベースだったりデジタル化途上！ということで、今後さらにたくさんのシステム導入事例が生まれるんじゃないでしょうか。
そんな中、やはりどうしても機械は古いものを使用し続けていたりということで、、レガシーな技術もある程度知っていれば役立つんじゃないかな！と思います！

## 一緒に働く仲間を募集中です！
リンクラフト株式会社では、組織拡大に伴い積極的な採用活動を行っています。
少しでも興味がある方はぜひご連絡ください。

▽会社ホームページ
https://lincraft.co.jp/
▽Instagram
https://www.instagram.com/lincraft.inc/
▽ご応募はこちらより
https://lincraft.co.jp/recruit

※カジュアル面談も受付中です。ご希望の方はHPのお問い合わせフォームよりご連絡ください。