C++で作成した「ネイティブアセンブリdll」の配置パスの追加

概要

前節では、C#等で作られた「中間アセンブリ」の場合を解説しました。

ここではさらに深堀して、「C#で自作するdllがさらにネイティブdllを呼び出す場合にそのパス」を追加する場合を解説します。
厳密にはパス自体を追加するわけではなく、dllを読み込んでしまうことで、以降C#の該当プロセスで自由に使えるようにする、というものです。

ネイティブのdllも同じように場所を指定する必要があります。

直接あなたが作るdllがそのネイティブを利用する場合も考えれますし、
nugetなどを利用して追加した比較的大型な参照アセンブリが、ネイティブdllにも依存している場合も考えられます。

その両方で今回の手法によって簡単に「extern」や「DllImport(***)」などの既存部分の書き換えなしで解決することが出来ます。

ネイティブdllのわかりにくい特性 ネイティブのdllはアセンブリのdllとは異なりカレントフォルダにあるdll」が読みこめてしまうため、後になってバグが起きやすい性質があります。

例えば、たまたま今、マクロファイル(例えばClassLibrary36.mac)を秀丸エディタで開いていて、同じディレクトリにdllがあれば、読み込めてしまいます。

しかし、全て秀丸を閉じたあと、マクロファイルを秀丸で開かずに、別のフォルダの別のファイル(例:Test.txt)を開き、いきなりマクロ(ClassLibrary36.mac)を実行した場合には、
カレントフォルダ(Test.txtがあるフォルダ)を探してもdllは無いわけですからエラーとなります。

マクロ制作中はネイティブdllが読み込めていたのに、いざ確認作業を始めると読み込めなくなった
というのはこの罠にはまり込んでいることが多いです。

ネイティブdllの作成

まずは、以下のようなネイティブdllを「dll1.dll」として作成し、これをC#側で利用したいとしましょう。

PICTURE

dllmain.cpp
// dllmain.cpp : DLL アプリケーションのエントリ ポイントを定義します。
#include "pch.h"
#include "windows.h"

BOOL APIENTRY DllMain( HMODULE hModule, DWORD  ul_reason_for_call, LPVOID lpReserved )
{
switch (ul_reason_for_call)
    {
        case DLL_PROCESS_ATTACH:
        case DLL_THREAD_ATTACH:
        case DLL_THREAD_DETACH:
        case DLL_PROCESS_DETACH:
            break;
    }
    return TRUE;
}

extern "C" __declspec(dllexport) intptr_t MyFunc(intptr_t parm1) {
    return (intptr_t)3;
}    

ネイティブdllのパス解決

自作のdllに、以下のような「NativeResolver」とも呼べるプログラムを、追加することで、「ネイティブdllを読ませる」ことが出来ます。

DllNativeResolver.cs
using System;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;

internal class DllNativeResolver
{
    [DllImport("kernel32")]
    private static extern IntPtr LoadLibrary(string lpFileName);

    const string dllName = "Dll1.dll";

    public DllNativeResolver()
    {
        var selfdir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);

        // x86とx64でサブフォルダで分けるといったことはよくあること。
        // var subDir = Environment.Is64BitProcess ? "x64" : "x86";
        // var dllPath = Path.Combine(selfdir, subDir, dllName);
        var dllPath = Path.Combine(selfdir, dllName);
        System.Diagnostics.Trace.WriteLine(dllPath);

        if (!(File.Exists(dllPath) && LoadLibrary(dllPath) != IntPtr.Zero))
        {
            System.Diagnostics.Trace.WriteLine(dllName + " の読み込みに失敗しました");
            throw new DllNotFoundException(dllPath);
        }
    }
}

秀丸マクロから最初に呼ぶ予定の「メソッドを含むクラス」の「静的コンストラクタ」に以下のようなものを記述することです。
ポイントは秀丸マクロから最初に呼ぶ予定の「メソッドを含むクラス」の「静的コンストラクタ」に書くことです。

MyTest.cs
using System;
using System.Runtime.InteropServices;

namespace MyTestNameSpace {

    public class MyTestClass
    {
        static DllNativeResolver dntvr; 
        // 秀丸から最初に呼ぶメソッドが含まれているクラスの「静的コンストラクタ(static付きのコンストラクタ)」
        // でネイティブdllリゾルバーオブジェクトを生成する。
        static MyTestClass() {
            dntvr = new DllNativeResolver();
            System.Diagnostics.Trace.WriteLine("MyTestClass 静的コンストラクタ実行");
        }

        // 秀丸から最初に呼ぶメソッド
        static IntPtr abc(IntPtr a)
        {
            IntPtr r = MyFunc((IntPtr)3);
            int b = (int)a + 3 + (int)r;
            return (IntPtr)b;
        }

        // DllNatvieResolver内でいちはやく解決されていれば、DllImportの形がそのまま使える。
        [DllImport("dll1")]
        static extern IntPtr MyFunc(IntPtr i);
    }
}
              

このようにして、dll1.dllの配置場所を自由に操作することができます。

PICTURE