суббота, 20 августа 2016 г.

Динамический вызов функций неуправляемых DLL

В .NET-атрибутах можно указывать только константные выражения. Т.о. атрибуту DllImport нужно указывать имя библиотеки статически, дабы это имя было известно на этапе компиляции. Порой это порождает уродливые конструкции, разрастающиеся как снежный ком, по мере появления новых версий AutoCAD. Наглядный тому пример можно увидеть здесь в коде Александра Ривилиса: по мере появления AutoCAD 2018, 2019 и т.д. - этот код придётся каждый раз дописывать.


Как правило, имя нужной DLL достаточно просто выяснить во время выполнения кода. Например, в обозначенном выше коде Александра Ривилиса нужная функция хранится в файлах acdb17.dll, acdb18.dll, ac1st19.dll, ac1st20.dll, ac1st21.dll. Т.е. мы видим, что каждая библиотека в качестве суффикса использует значение Major версии приложения. Кроме того, мы видим, что начиная с версии 19 (AutoCAD 2013) функция была перенесена в др. библиотеку (т.е. изменился префикс в её имени). Зная обозначенные выше правила, можно динамически вычислить имя DLL файла, в котором находится интересующая нас функция.

Далее показываю небольшой пример того, как можно динамически вызывать функцию acedSetEnv в разных версиях AutoCAD, не создавая для неё статических обёрток. На всякий случай напоминаю, что до версии AutoCAD 2013 эта функция находилась в файле acad.exe, а начиная с AutoCAD 2013 была перенесена в accore.dll.

Код проверялся в AutoCAD 2009 и 2016.

-------------------------------------------------------------
/* Utils.cs */
using System;
using System.Text;
using System.Runtime.InteropServices;
using cad = Autodesk.AutoCAD.ApplicationServices.Application;

namespace Bushman.AutoCAD.Sandbox {

    [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet
        = CharSet.Auto)]
    public delegate int acedSetEnvDelegate(string envName,
        StringBuilder NewValue);

    public static class Utils {

        [DllImport("kernel32.dll")]
        public static extern IntPtr LoadLibrary(string
            dllToLoad);

        [DllImport("kernel32.dll")]
        public static extern IntPtr GetProcAddress(IntPtr
            hModule, string procedureName);


        [DllImport("kernel32.dll")]
        public static extern bool FreeLibrary(IntPtr hModule);

        const int AutoCAD_2013_Major = 19;

        public static string GetDllName() {

            if (cad.Version.Major < AutoCAD_2013_Major)
                return "acad.exe";
            else
                return "accore.dll";
        }

        public static acedSetEnvDelegate acedSetEnv;

        static Utils() {
            acedSetEnv = GetAcedSetEnv();
        }

        static acedSetEnvDelegate GetAcedSetEnv() {

            string dllName = Utils.GetDllName();
            IntPtr pDll = Utils.LoadLibrary(dllName);

            if (pDll == IntPtr.Zero) {
                return null;
            }

            string funcName = "acedSetEnv";

            IntPtr pAddressOfFunctionToCall = Utils
                .GetProcAddress(pDll, funcName);

            if (pAddressOfFunctionToCall == IntPtr.Zero) {

                return null;
            }

            acedSetEnvDelegate acedSetEnv = (
                acedSetEnvDelegate) Marshal
                .GetDelegateForFunctionPointer(
                pAddressOfFunctionToCall,
                typeof(acedSetEnvDelegate));

            bool result = Utils.FreeLibrary(pDll);

            return acedSetEnv;
        }
    }
}

 ------------------------------------------------------------

Далее создадим тестовую команду, которая изменит значение переменной "ACAD":

------------------------------------------------------------
/* Commands.cs */
using System;
using System.Runtime.InteropServices;
using System.Text;
using cad = Autodesk.AutoCAD.ApplicationServices.Application;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;

namespace Bushman.AutoCAD.Sandbox {

    public sealed class Commands {

        [CommandMethod("Test", CommandFlags.Modal)]
        public void Test() {

            Document doc = cad.DocumentManager
                .MdiActiveDocument;

            if (doc == null)
                return;

            Editor ed = doc.Editor;

            acedSetEnvDelegate acedSetEnv = Utils.acedSetEnv;

            if (acedSetEnv == null) {
                ed.WriteMessage("acedSetEnv function was " +
                    "not found.");
                return;
            }

            /* For example, we'll edit the "Support File Search
             * Path" value: */
            string varName = "ACAD";
            StringBuilder sb = new StringBuilder(
                @"C:\abc\def");

            int theResult = acedSetEnv(varName, sb); // 5100
        }
    }
}

---------------------------------------------------------------

Динамическое связывание и изменение системой переменной ACAD успешно происходит в обоих тестируемых версиях AutoCAD: 2009 и 2016.



Комментариев нет: