Pull to refresh

Вызов managed кода из unmanaged

Reading time 4 min
Views 7K
В данной статье будет рассматриваться вызов управляемого C# — кода(.Net) из неуправляемого С — кода.
Как-то раз на работе дали проект, точнее даже не сам проект, а только его часть. Сам же проект состоял из двух частей: функционал, написанный на С (unmanaged code) и интерфейсная часть, написанная на C# (managed code). Моей задачей было написать интерфейс и связать его с функционалом.

Далее в этой статье managed code будет называться верхним уровнем, unmanaged – нижним.

Как известно, для обращения к нижнему уровню с верхнего в C# используется механизм P/Invoke(Platform Invoke). Для этого нижний уровень оборачивается в Dll (то есть все функции нижнего уровня делаются экспортируемыми) и вызывается сверху с помощью атрибута DllImport. Для тех, кто незнаком с данным механизмом — msdn.microsoft.com/en-us/library/aa288468(v=vs.71).aspx

В ходе выполнения задания передо мной встала проблема – на нижнем уровне находилась функция обратного вызова, которая должна была уведомить верхний уровень об удачном или неудачном завершении функции. Для решения проблемы нужно было либо вызвать с нижнего уровня верхний уровень либо придумать какой-либо механизм, позволяющий узнать момент вызова функции верхнего уровня (например, при помощи события). Поиск в интернете по теме вызова managed кода из unmanaged не принес должных плодов. Тем не менее было решено попробовать обойтись малой кровью и не изобретать велосипед.

Для упрощения понимания был создан новый солюшн, включающий два проекта: проект низкого(CallC) и проект высокого уровня(SharpForCall).

Итак, у нас есть пустой проект C#( Console Application) и кем-то написанный проект на С (у меня изначально конечно же был только h-file, берем сразу проект для простоты). Тип проекта на С – Dll, которая естественно должна лежать рядом с нашим экзешником, полученным в C#. В проекте есть *.cpp файл следующего содержания:

/** Обратный уведомляющий вызов */
typedef 
VOID (__stdcall * pResultCallBack)(
	int nStatus
	);

__declspec(dllexport)
int FuncC(pResultCallBack pfResult, PVOID pContext)
{
	//
	// здесь что-то делаем
	//

	/* перед выходом из функции уведомляем высший уровень(C#) о результате */
	pfResult(1);

	return 1;
}


Еще раз поясню смысл того, что нужно сделать. Экспортируемая функция здесь(FuncC) будет импортируемой на стороне C#, которая, предположим, вызовется при нажатии пользователем какой-нибудь кнопки (не будем забывать, что главной задачей является связь интерфейса с функционалом). Эта функция (импортируемая на стороне C#) вызовет, естественно, функцию FuncC в данном файле *.cpp (см. выше), которая после выполнения должна сообщить результат выполнения назад в C# при помощи вызова функции pResultCallBack. На стороне верхнего уровня функция pResultCallBack (в нашем случае FuncCSharp, см ниже) будет анализировать результат выполнения функции FuncC и в зависимости от переданного ей значения выполнять определенные действия (например, при возврате кода состояния, сообщающего о неудачном вызове, можно повторить вызов и т.д.). Вообще данная идея может использоваться для управления одной машиной (хостом) другой машиной.

Приступим к реализации.

Во-первых, заходим в настройки С-шного проекта в Configuration Properties->General->Output Directory и пишем путь к папке с экзешником проекта C#.

image

Во-вторых, не забываем зайти в Project Dependencies проекта C# и поставить там галочку рядом с С-проектом.

image

Далее, создаем класс Import.cs, в котором описываем импортируемую функцию при помощи механизма P/Invoke.

using System;
using System.Runtime.InteropServices;// не забываем подключить

namespace SharpForCall
{
    class Import
    {
        [DllImport("CallC.dll", CallingConvention = CallingConvention.Cdecl)]
        public static extern int FuncC(
            [MarshalAs(UnmanagedType.FunctionPtr)] ResultCallBack pfResult,
            IntPtr pContext);
    }
}


PVOID заменяем на IntPtr, а указатель на функцию pResultCallBack на делегат ResultCallBack, который описан в файле Program.cs следующим образом:

using System;

namespace SharpForCall
{
    public delegate void ResultCallBack(Int32 nStatus);

    class Program
    {
        static void Main(string[] args)
        {
            ResultCallBack result = new ResultCallBack(FuncCSharp);

            IntPtr pContext = IntPtr.Zero;

            /* Вызов функции из Dll */
            int nRes = Import.FuncC(result, pContext);
        }

        /** Функция анализа обратного вызова.
         *  Вызывается из unmanaged кода.
         */
        public static void FuncCSharp(Int32 nStatus)
        {
            if ( 1 == nStatus )
            {
                //бла бла бла
            }
            if ( 2 == nStatus )
            {
                //бла бла бла
            }
            // и так далее

            return ;
        }
    }
}


Теперь, запустив программу и пройдясь по ней по шагам (для того чтобы войти в unmanaged код нужно установить чекбокс в свойствах проекта -> Debug->Enable unmanaged code debugging), мы увидим, что сначала верхний уровень вызывает нижний, передавая ему (нижнему уровню) делегат, а нижний – по окончании выполнения функции FuncC вызывает верхний (функцию FuncCSharp) по тому самому «делегату» и передает ему результат выполнения функции (в данном случае «1»). Далее функция анализирует полученный код состояния и возвращает управление на нижний уровень, оттуда управление передается на верхний уровень. Если при выполнении программа выдает ексепшн следующего содержания:

image

, то дописываем на стороне С в определении колбека __stdcall.

Если и это не помогло, то на стороне C# в классе Import нужно дописать CallingConvention = CallingConvention.Cdecl при вызове атрибута DllImport. Это все нужно для того, чтобы вернуть стек в исходное состояние.

Теперь все работает. Мы только что совершили то, что многие считают невозможным – вызвали managed код из unmanaged.

PS: Думаю, кому-нибудь пригодится. Жду ваших комментариев…
Tags:
Hubs:
+21
Comments 25
Comments Comments 25

Articles