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

В данной статье будет рассматриваться вызов управляемого 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: Думаю, кому-нибудь пригодится. Жду ваших комментариев…
Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 25

    +4
    You are insane!
      +1
      согласен с вами, что подход необычный для анализа возвращаемого значения… можно было бы просто из функции возвращать и проверять… но передо мной встала именно эта проблема, так как функционал писал начальник, а против него не попрешь((
      0
      Я не знаток C#.
      Попробуйте, пожалуйста, в вашей функции сделать что-нибудь, что требует managed окружения — бросить исключение, например?
        0
        Или тред создать…
          0
          вы имеете ввиду функцию «FuncCSharp»??
            0
            Нет, я так понимаю, человек хочет попросить Вас провести эксперимент по использованию несколько более сложных функций из стандартного набора .NET
              0
              Да, именно из нее. Просто вы не передаете в нее явным образом никакого managed context'а, что может и должно привести к проблемам с исполнением настоящего управляемого кода (исключения, треды).
                +1
                Как раз не должно. О потоках фреймворк позаботится за вас. Thread.Curent.ManagedId вернет корректный результат, и все прочие функции работы с потоками будут корректно работать.
                  +1
                  вобщем, я попробовал(тред и исключение кинуть в функции)… как я и ожидал, ничего не случилось… программа как работала, так и работает… Собственно, ответ на ваш вопрос очень точно описан ниже человеком с ником — kekekeks
                    0
                    Спасибо, надо будет как-нибудь покопаться в этом деле.
                +1
                Вообще говоря в C# всё нормально с передачей в неуправляемый код указателей на функции и с колбеками из не ассоциированных с managed-окружением потоков. Т. е. оно вам и LPWSTR в System.String переведёт, и результат как надо сконвертит при желании. Единственное, чего нельзя никогда ни при каких обстоятельствах делать — это кидать исключения в unmanaged код из managed-кода и наоборот.
                  0
                  нормально все работает, у нас в аналоге FuncCSharp обращение к БД идет, проблем нет.
                  +3
                  Эт вы ещё из C# не работали с псевдо-COM-интерфейсами, которые кидают E_NOTIMPL при попытке вызвать QueryInterface (кстати, это поделие было за авторством мелкомягких и весьма активно использовалось). А CLR его вызывает всегда при попытке скастовать что-либо к интерфейсу. В итоге пришлось ручками расковыривать vtable, создавать делегаты для указателей на методы, а потом ещё и оборачивать всё это безобразие, чтобы скрыть первый параметр. В общем, во взаимодействии managed и unmanaged кода много весёлых вещей есть.
                    0
                    Некоторые проекты, вместо QueryInterface, при сложной изменчивой структуре объектов, используются другие механизмы получения. Например getInterface в XPCOM. То что вы делаете: натурально хак.
                      0
                      Соль в том, что если есть объект IMyCoolObject, который возвращается из функи, то при вызове QueryInterface с IID_IMyCoolObject он должен выдать тот самый IMyCoolObject и никак иначе.
                        0
                        Вы правы, объект плохой.
                    +1
                    очень интересный подход)
                    а не пробовали все-таки перевести это дело на C++/CLI?
                      0
                      Проще описать интерфейсы в idl, после чего сгенерить из него входящей в SDK утилитой заголовки для C/C++ и tlbшку для .NET.
                        0
                        да, tlbшки очень помогают. также пробовал. однако иногда легче создать и использовать managed handle, а всю работу завернуть в какую-либо обертку.
                        +1
                        не пробовал… если честно, не очень его люблю… Да и задача была поставлена так, что писать именно на C#…
                        0
                        Поиск в интернете по теме вызова managed кода из unmanaged не принес должных плодов
                        Я конечно извиняюсь но запрос calling managed code from unmanaged вроде должный вариант первым линком выдает.
                          0
                          думаю, что сейчас все можно найти через поиск. в данной статье автор поделился своим опытом. также, как Вы уже заметили, все линки ведут на статьи на английском языке.
                          +1
                          Мне кажется эта статья была бы более уместна в блоге .NET
                            +1
                            То есть статья просто о маршаллинге делегатов в указатели на функции? Может я оптимист, но от такого громкого названия ожидал большего.
                              +1
                              перенес топик в .Net, где ему и место…

                              Only users with full accounts can post comments. Log in, please.