Маршалинг данных между управляемым и неуправляемым кодом - Обратный вызов P/Invoke и время существования делегата

ОГЛАВЛЕНИЕ


Обратный вызов P/Invoke и время существования делегата

Среда CLR позволяет передать делегат в неуправляемый код, чтобы тот мог обращаться к нему как к неуправляемому указателю функции. В сущности, среда CLR создает преобразователь адресов, который направляет вызовы из машинного кода к фактическому делегату, а затем к самой функции (см. рис. 10).


Рис. 10 Использование преобразователя

Обычно о времени существования делегата задумываться не приходится. При передаче делегата в неуправляемый код среда CLR гарантирует его существование на протяжении всего вызова.

Сложности возникают тогда, когда машинный код сохраняет копию указателя даже после завершения вызова и пытается впоследствии осуществить обратный вызов по этому указателю: в таком случае иногда приходится использовать GCHandle явным образом, чтобы сборщик мусора не подобрал делегат. Нужно иметь в виду, что при фиксации GCHandle может значительно снизиться производительность программы. К счастью, в нашем случае выделять фиксированный дескриптор GC не приходится, поскольку преобразователь адресов выделяется в неуправляемой куче и ссылается на делегат через ссылку, известную GC, поэтому преобразователь адресов перемещаться не может, и машинный код в любой момент способен вызвать делегат через неуправляемый указатель (если сам делегат еще существует).

Функция Marshal.GetFunctionPointerForDelegate преобразовывает делегат в указатель функции, но она никаким образом не гарантирует существование делегата в течение нужного срока. Обратите внимание на следующее объявление функции.

public delegate void PrintInteger(int n);

[DllImport(@"MarshalLib.dll", EntryPoint="CallDelegate")]
public static extern void CallDelegateDirectly(
IntPtr printIntegerProc);

Если вызвать Marshal.GetFunctionPointerForDelegate для этой функции и сохранить возвращенное значение IntPtr, то затем вы передадите его в функцию, которую вы собираетесь вызывать, следующим образом.

IntPtr printIntegerCallback = Marshal.GetFunctionPointerForDelegate(
new Lib.PrintInteger(MyPrintInteger));

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

CallDelegateDirectly(printIntegerCallback);

Существует возможность того, что делегат будет удален сборщиком мусора прежде, чем вы вызовете функцию CallDelegateDirectly. Тогда вы получите сообщение MDA о том, что обнаружено событие CallbackOnCollectedDelegate. Чтобы исправить эту ошибку, нужно либо сохранить в памяти ссылку на делегат, либо выделить дескриптор GC.

Если машинный код возвращает среде CLR неуправляемый указатель функции, то за сохранение кода функции отвечает машинный код. Здесь обычно проблем не возникает — если, конечно, код не находится в динамически загружаемой библиотеке DLL и не создается оперативно.