Как написать собственный маршалер, который позволяет передавать данные из native в управляемый?
При попытке написать собственный маршалер, связанный с этим вопросом (P/Invoke от C до С# без знания размера массива), я столкнулся с чем-то, что не могу Понимаю. Это первый в истории пользовательский маршалер, который я написал, поэтому, без сомнения, я пропустил что-то очевидное из-за моего невежества.
Здесь мой код С#:
using System;
using System.Runtime.InteropServices;
using System.Text;
namespace CustomMarshaler
{
public class MyCustomMarshaler : ICustomMarshaler
{
static MyCustomMarshaler static_instance;
public IntPtr MarshalManagedToNative(object managedObj)
{
if (managedObj == null)
return IntPtr.Zero;
if (!(managedObj is int[]))
throw new MarshalDirectiveException("VariableLengthArrayMarshaler must be used on an int array.");
int[] arr = (int[])managedObj;
int size = sizeof(int) + arr.Length * sizeof(int);
IntPtr pNativeData = Marshal.AllocHGlobal(size);
Marshal.WriteInt32(pNativeData, arr.Length);
Marshal.Copy(arr, 0, pNativeData + sizeof(int), arr.Length);
return pNativeData;
}
public object MarshalNativeToManaged(IntPtr pNativeData)
{
int len = Marshal.ReadInt32(pNativeData);
int[] arr = new int[len];
Marshal.Copy(pNativeData + sizeof(int), arr, 0, len);
return arr;
}
public void CleanUpNativeData(IntPtr pNativeData)
{
Marshal.FreeHGlobal(pNativeData);
}
public void CleanUpManagedData(object managedObj)
{
}
public int GetNativeDataSize()
{
return -1;
}
public static ICustomMarshaler GetInstance(string cookie)
{
if (static_instance == null)
{
return static_instance = new MyCustomMarshaler();
}
return static_instance;
}
}
class Program
{
[DllImport(@"MyLib.dll")]
private static extern void Foo(
[In, Out, MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(MyCustomMarshaler))]
int[] arr
);
static void Main(string[] args)
{
int[] colorTable = new int[] { 1, 2, 3, 6, 12 };
Foo(colorTable);
foreach (int value in colorTable)
Console.WriteLine(value);
}
}
}
С другой стороны, это тривиальная встроенная DLL, написанная в Delphi, когда это происходит.
library MyLib;
procedure Foo(P: PInteger); stdcall;
var
i, len: Integer;
begin
len := P^;
Writeln(len);
for i := 1 to len do begin
inc(P);
Writeln(P^);
inc(P^);
end;
end;
exports
Foo;
begin
end.
Идея состоит в том, что массив передается в DLL, которая затем выводит поле длины и значения массива. Нативный код также увеличивает каждое значение массива на 1.
Итак, я ожидаю увидеть этот вывод:
5
1
2
3
6
12
2
3
4
7
13
Но, к сожалению, я вижу этот вывод:
5
1
2
3
6
12
1
2
3
6
12
В отладчике я вижу, что выполняется MarshalNativeToManaged
, и что возвращаемые значения увеличиваются. Но эти добавочные значения не возвращаются обратно в объект, который передается в Foo
.
Что мне нужно сделать, чтобы исправить это?
Ответы
Ответ 1
У меня была аналогичная проблема много лет назад, и выяснилось, что очень мало документации по Custom Marshaling. Я подозреваю, что использование ICustomMarshaler никогда не снималось, поскольку это всегда можно сделать с помощью ручного маршалинга в ходе вашего обычного кода. И поэтому никогда не было необходимости в какой-либо документации по расширенным пользовательским сценариям маршалинга.
В любом случае, через множество источников и много проб и ошибок, я думаю, что я поддразнил практическое понимание того, как работает большинство Custom Marshaling.
В вашем случае вы правильно настроили метод ManagedToNative для марксирования [In] и правильного метода NativeToManaged для большинства [Out] маршалингов, но [In, Out] маршалинг на самом деле немного сложнее. [In, Out] маршалинг - это на самом деле маршалинг на месте. Таким образом, на обратном пути вы должны отправить данные обратно в тот же экземпляр, который был предоставлен в [In] стороне операции.
В этом случае существует несколько небольших вариаций в зависимости от того, используется ли ссылочный или тип значений, является ли вызов обычным вызовом pInvoke или обратным вызовом для делегата и т.д. Но размышление о том, что нужно, чтобы в конечном итоге было ключом.
Следующая вариация вашего кода работает так, как вы хотите (и, похоже, она работает одинаково для .Net 2.0 и выше):
//This must be thread static since, in theory, the marshaled
//call could be executed simultaneously on two or more threads.
[ThreadStatic] int[] marshaledObject;
public IntPtr MarshalManagedToNative(object managedObj)
{
if (managedObj == null)
return IntPtr.Zero;
if (!(managedObj is int[]))
throw new MarshalDirectiveException("VariableLengthArrayMarshaler must be used on an int array.");
//This is called on the way in so we must keep a reference to
//the original object so we can marshal to it on the way out.
marshaledObject = (int[])managedObj;
int size = sizeof(int) + marshaledObject.Length * sizeof(int);
IntPtr pNativeData = Marshal.AllocHGlobal(size);
Marshal.WriteInt32(pNativeData, marshaledObject.Length);
Marshal.Copy(marshaledObject, 0, (IntPtr)(pNativeData.ToInt64() + sizeof(int)), marshaledObject.Length);
return pNativeData;
}
public object MarshalNativeToManaged(IntPtr pNativeData)
{
if (marshaledObject == null)
throw new MarshalDirectiveException("This marshaler can only be used for in-place ([In. Out]) marshaling.");
int len = Marshal.ReadInt32(pNativeData);
if (marshaledObject.Length != len)
throw new MarshalDirectiveException("The size of the array cannot be changed when using in-place marshaling.");
Marshal.Copy((IntPtr)(pNativeData.ToInt64() + sizeof(int)), marshaledObject, 0, len);
//Reset to null for next call;
marshalledObject = null;
return marshaledObject;
}
Ответ 2
int len = Marshal.ReadInt32(pNativeData);
int[] arr = new int[len];
Ваша проблема здесь, вы создаете новый массив. Но вам нужно обновить массив colorTable. Вы получили ссылку на него в методе MarshalManagedToNative(), вам нужно будет его сохранить, чтобы вы могли использовать его снова в методе MarshalNativeToManaged().
Обратите внимание, что это имеет ряд последствий. Ваш пользовательский объект marshaller становится работоспособным, вы больше не можете использовать статический экземпляр. И вам понадобится другой подход, если неуправляемый код изменяет длину массива. Вы разрешаете это, читая ленту назад, но на самом деле не реализуете ее, так что все в порядке. Просто утверждают, что длина не изменилась.
Ответ 3
Большое спасибо Стивену и Хансу за отличные ответы. Теперь я могу ясно видеть, что я должен держать управляемый объект, который был передан в MarshalManagedToNative
, а затем вернуть тот же объект из последующего вызова в MarshalNativeToManaged
.
Это немного привязка, которая не обеспечивает механизм управления этим состоянием. Это потому, что маршаллер использует один и тот же экземпляр пользовательского маршаллера для каждого вызова функции.
Стивенский подход использования локального хранилища потоков будет работать, я считаю. Я лично не поклонник локального хранилища потоков. Другим вариантом является использование словаря, наложенного на неуправляемый указатель. Вот иллюстрация:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
namespace CustomMarshaler
{
public class MyCustomMarshaler : ICustomMarshaler
{
private Dictionary<IntPtr, object> managedObjects = new Dictionary<IntPtr, object>();
public IntPtr MarshalManagedToNative(object managedObj)
{
if (managedObj == null)
return IntPtr.Zero;
if (!(managedObj is int[]))
throw new MarshalDirectiveException("MyCustomMarshaler must be used on an int array.");
int[] arr = (int[])managedObj;
int size = sizeof(int) + arr.Length * sizeof(int);
IntPtr pNativeData = Marshal.AllocHGlobal(size);
Marshal.WriteInt32(pNativeData, arr.Length);
Marshal.Copy(arr, 0, pNativeData + sizeof(int), arr.Length);
lock (managedObjects)
{
managedObjects.Add(pNativeData, managedObj);
}
return pNativeData;
}
public object MarshalNativeToManaged(IntPtr pNativeData)
{
int[] arr;
lock (managedObjects)
{
arr = (int[])managedObjects[pNativeData];
managedObjects.Remove(pNativeData);
}
int len = Marshal.ReadInt32(pNativeData);
Debug.Assert(len == arr.Length);
Marshal.Copy(pNativeData + sizeof(int), arr, 0, len);
return arr;
}
public void CleanUpNativeData(IntPtr pNativeData)
{
Marshal.FreeHGlobal(pNativeData);
}
public void CleanUpManagedData(object managedObj)
{
}
public int GetNativeDataSize()
{
return -1;
}
public static ICustomMarshaler GetInstance(string cookie)
{
return new MyCustomMarshaler();
}
}
class Program
{
[DllImport(@"MyLib.dll")]
private static extern void Foo(
[In, Out, MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(MyCustomMarshaler))]
int[] arr
);
static void Main(string[] args)
{
int[] colorTable = new int[] { 1, 2, 3, 6, 12 };
Foo(colorTable);
foreach (int value in colorTable)
Console.WriteLine(value);
}
}
}