.NET и работа с неуправляемым кодом. Часть 2
Тем, кто не читал первую часть — читать
После выполнения первой части работ, создания структур, мне задачу усложнили. Библиотечка с которой мне нужно было работать — выделяла массив, размер которых не был известен до момента запуска. Он был динамический, т.е. чем больше данных — тем больше массив. Тут задача стала поинтереснее, т.к. старый способ, когда было достаточно использовать структуру, в которой этот размер был указан — уже не подходил.
Тогда я стал дальше изучать маршалинг и нашел еще несколько методов у класса Marshal, которые помогли мне решить задачу.
И так, собственно сама задача:
— библиотечка на входе принимает указатель на массив указателей (void*) o_O
— содержимое массива — этой указатели на другие массивы, например на массив символов, где указан адрес файла, указатели на массивы int, в которых содержаться некоторые данные
Т.к. мне часто пришлось бы вызывать некоторый код, я создал специальный статический класс UnMemory, его код ниже
В этом классе я определил очередь, в которую буду добавлять все указатели на сгенерированную неуправляемую память, чтобы в конце вызывать его статический метод FreeMomory() для полной очистки выделенной памяти. Для добавления указателя в очередь, необходимо вызвать UnMemory.Enqueue(ptr);
Мне показалось это удобнее, чем обращаться к указателям, чтобы очистить каждый из них. Ведь так можно что-то пропустить и получим утечку памяти.
Также, мне понадобился еще один класс, который я назвад UnMemory, который будет выделять в неуправляемой памяти место и заполняеть его данными, ну и конечно читать из неё.
Его код ниже
Этот класс обобщенный, что позволило не создавать множество методов для каждого типа отдельно. Смысл этого класса, он повереводит массивы из управляемой памяти в неуправляемую и наоборот. Метод SaveInMem сохраняет структуру, которую мы ему передадим, метод SaveInMem2 сохранит массив, таких как intPtr, Int, float… Ограничение представляет сам метод Marshal.Copy, в котором реализовано копирование отдельно для int, отдельно для byte и некоторых других. Поэтмоу нужно было для каждого типа реализовать свой свой вызов, такой как if (typeof(T) == typeof(int))
Методы ReadInMem и ReadInMem2 для чтения структур и массивов, аналогично SaveMem и SaveMem2.
Как я говорил в первой части статьи, мне приходилось работать с многомерными массивами, но маршалинг не позволяет это делать, он может читать и писать только одномерные массивы. Поэтому я создать подкласс UnArray, который также обобщенный, имеет несколько методов, которые создают массивы из одномерного в двумерный, трехмерный, четырехмерный и наоборот. Правда все эти методы мне нужны были больше в первой части, во второй, при работе с массивами почти не пригодились, хотя можно было бы и несколько методов добавить, тогда пригодились бы.
И так, теперь переходим непосредственно к самому вызову.
Из С, вызов выгладит так:
Для C# этот вызов будет выглядить так:
Осталось самое простое. Сделать обертку для использования
ОБРАТИТЕ ВНИМАНИЕ, что для простого текста, строки, я использовал Marshal.StringToHGlobalAnsi(text). Т.к. это специальный метод класса Marshal для чтения и записи обычной текстовой строки, которая оканчивается символом '\0'
Массив указателей сохраняется в mpSh, а для всех полей реализованы методы get и set, которые почти прозрачно от самого класса пишут и читают неуправляемую память. Метод Read переносит массив указателей в неуправляемую память, чтобы потом прочесть данные из неё (при вызове библиотеки, эти данные заполняются). После вызова Read, мы сможем прочесть нужную нам переменную просто обратившись к нужному полю.
Правда есть одно ограничение… лучше получить ссылку на массив ( int[] paramSh = this.ParamSh ), а уже потом работать с ним, чем обращаться к полю постоянно ( this.size_vetv = this.ParamSh[0] ). Потому как при обращении, данные заново будут читаться из неуправляемой памяти. Несмотря, что это проиходит быстро, если вызовов много, это может занять достаточное время.
PS: в рамках данной статьи не был рассмотрен CustomMarshaling. Суть его в том, что Вы можете польностью настраивать, как должна сохраняться определенная структура, с его помощью Вы можете поддерживать различные версии своих библиотек. Например, сохранять DateTime как String в определенном формате и пр. Такие случаи бывают реже, поэтому этому можно посветить отдельную статью.
К сожалению приложить исходники к статье не могу, т.к. это не одобрит руководство :) Но я постарался привести максимум кода, чтобы без исходников было понятно, как работать с маршалингом. Если вам будем что-то непонятно, пожалуйста напишите и я отвечу на ваши вопросы.
Тем, кто не читал первую часть — читать
После выполнения первой части работ, создания структур, мне задачу усложнили. Библиотечка с которой мне нужно было работать — выделяла массив, размер которых не был известен до момента запуска. Он был динамический, т.е. чем больше данных — тем больше массив. Тут задача стала поинтереснее, т.к. старый способ, когда было достаточно использовать структуру, в которой этот размер был указан — уже не подходил.
Тогда я стал дальше изучать маршалинг и нашел еще несколько методов у класса Marshal, которые помогли мне решить задачу.
И так, собственно сама задача:
— библиотечка на входе принимает указатель на массив указателей (void*) o_O
— содержимое массива — этой указатели на другие массивы, например на массив символов, где указан адрес файла, указатели на массивы int, в которых содержаться некоторые данные
Т.к. мне часто пришлось бы вызывать некоторый код, я создал специальный статический класс UnMemory, его код ниже
/// <summary>
/// Класс для очистки блоков неуправляемой памяти
/// </summary>
public static class UnMemory
{
/// <summary>
/// Очередь для высвобождения блоков памяти
/// </summary>
private static Queue<IntPtr> queue = new Queue<IntPtr>();
public static void Enqueue(IntPtr ptr)
{
queue.Enqueue(ptr);
}
private static void FreeIntPtr(IntPtr ptr)
{
if (ptr != IntPtr.Zero)
Marshal.FreeCoTaskMem(ptr);
}
/// <summary>
/// Освобождение блоков памяти в неуправляемом пространстве
/// </summary>
public static void FreeMemory()
{
while (queue.Count > 0)
{
IntPtr temp = queue.Dequeue();
// освобождаем то, что записано в памяти
Marshal.FreeCoTaskMem(temp);
}
}
}
* This source code was highlighted with Source Code Highlighter.В этом классе я определил очередь, в которую буду добавлять все указатели на сгенерированную неуправляемую память, чтобы в конце вызывать его статический метод FreeMomory() для полной очистки выделенной памяти. Для добавления указателя в очередь, необходимо вызвать UnMemory.Enqueue(ptr);
Мне показалось это удобнее, чем обращаться к указателям, чтобы очистить каждый из них. Ведь так можно что-то пропустить и получим утечку памяти.
Также, мне понадобился еще один класс, который я назвад UnMemory, который будет выделять в неуправляемой памяти место и заполняеть его данными, ну и конечно читать из неё.
Его код ниже
/// <summary>
/// Класс для работы неуправляемой памятью
/// </summary>
/// <typeparam name="T">Структурный тип данных</typeparam>
public static class UnMemory<T>
where T : struct
{
/// <summary>
/// Получить указатель на структуру в неуправляемом куску памяти
/// </summary>
/// <param name="memory_object">Объект для сохранения</param>
/// <param name="ptr">Указатель</param>
/// <typeparam name="T">Структурный тип данных</typeparam>
public static void SaveInMem(T memory_object, ref IntPtr ptr)
{
if (default(T).Equals(memory_object))
{
// объявляем указатель на кусок памяти
ptr = Marshal.AllocCoTaskMem(Marshal.SizeOf(typeof(T)));
UnMemory.Enqueue(ptr);
return;
}
if (ptr == IntPtr.Zero)
{
// объявляем указатель на кусок памяти
ptr = Marshal.AllocCoTaskMem(Marshal.SizeOf(typeof(T)));
// записываем в память данные структуры
Marshal.StructureToPtr(memory_object, ptr, false);
}
else
{
// записываем в память данные структуры
Marshal.StructureToPtr(memory_object, ptr, true);
}
UnMemory.Enqueue(ptr);
}
/// <typeparam name="T">IntPtr, int, float</typeparam>
/// <exception cref="System.ArgumentException">Параметр #1 должен быть массивом IntPtr, int, float</exception>
public static void SaveInMem2(T[] managedArray, ref IntPtr pnt)
{
Debug.Assert(managedArray != null, "Объект не должен быть Null");
Debug.Assert(managedArray.Length != 0, "Объект не может иметь длину массива 0");
if (pnt == IntPtr.Zero)
{
// объявляем указатель на кусок памяти. Размер = размер одного элемента * количество
//int size = Marshal.SizeOf(typeof(T)) * managedArray.Length;
int size = Marshal.SizeOf(managedArray[0]) * managedArray.Length;
pnt = Marshal.AllocCoTaskMem(size);
}
// в зависимости от типа массива, мы вызываем соответствующий метод в Marshal.Copy
if (typeof(T) == typeof(int))
{
int[] i = managedArray as int[];
Marshal.Copy(i, 0, pnt, i.Length);
}
else if (typeof(T) == typeof(byte))
{
byte[] b = managedArray as byte[];
Marshal.Copy(b, 0, pnt, b.Length);
}
else if (typeof(T) == typeof(float))
{
float[] f = managedArray as float[];
Marshal.Copy(f, 0, pnt, f.Length);
}
else if (typeof(T) == typeof(char))
{
// читаем массив байтов и переводим в текущую кодировку
byte[] b = Encoding.Default.GetBytes(managedArray as char[]);
Marshal.Copy(b, 0, pnt, b.Length);
}
else if (typeof(T) == typeof(IntPtr))
{
IntPtr[] p = managedArray as IntPtr[];
Marshal.Copy(p, 0, pnt, p.Length);
}
else
throw new ArgumentException("Параметр #1 должен быть массивом IntPtr, int, float или char");
// запоминаем указатель, чтобы потом его почистить
UnMemory.Enqueue(pnt);
}
/// <summary>
/// Чтение структуры из неуправляемой памяти
/// </summary>
/// <param name="ptr">Указатель</param>
/// <param name="type">Тип данных для чтения</param>
/// <returns>Структура из памяти</returns>
public static T ReadInMem(IntPtr ptr)
{
return (T)Marshal.PtrToStructure(ptr, typeof(T));
}
public static T[] ReadInMem2(IntPtr ptr, int size)
{
if (typeof(T) == typeof(int))
{
int[] memInt = new int[size];
Marshal.Copy(ptr, memInt, 0, size);
return memInt as T[];
}
else if (typeof(T) == typeof(byte))
{
byte[] memByte = new byte[size];
Marshal.Copy(ptr, memByte, 0, size);
return memByte as T[];
}
else if (typeof(T) == typeof(float))
{
float[] memFloat = new float[size];
Marshal.Copy(ptr, memFloat, 0, size);
return memFloat as T[];
}
else if (typeof(T) == typeof(IntPtr))
{
IntPtr[] memIntPtr = new IntPtr[size];
Marshal.Copy(ptr, memIntPtr, 0, size);
return memIntPtr as T[];
}
else
throw new ArgumentException("Параметр #1 должен быть массивом int, float или char");
}
/// <summary>
/// Класс переводит массивы
/// </summary>
public static class UnArray
{
/// <summary>
/// Перевод одномерного массива в двумерный
/// </summary>
/// <typeparam name="T">Тип исходного массива</typeparam>
/// <param name="array">Исходный массив</param>
/// <returns>Двумерный массив</returns>
public static T[,] Rank1_Rank2(T[] array, int x, int y)
{
T[,] res = new T[x, y];
int size = Buffer.ByteLength(array);
Buffer.BlockCopy(array, 0, res, 0, size);
return res;
}
/// <summary>
/// Перевод двумерного в одномерный массив
/// </summary>
/// <typeparam name="T">Тип исходного массива</typeparam>
/// <param name="array">Исходный массив</param>
/// <returns>Одномерный массив</returns>
public static T[] ToRank1(T[,] array, int x, int y)
{
T[] res = new T[x * y];
int size = Buffer.ByteLength(array);
Buffer.BlockCopy(array, 0, res, 0, size);
return res;
}
/// <summary>
/// Перевод одномерного массива в трехмерный
/// </summary>
/// <typeparam name="T">Тип исходного массива</typeparam>
/// <param name="array">Исходный массив</param>
/// <returns>Трехмерный массив</returns>
public static T[, ,] Rank1_Rank3(T[] array, int x, int y, int z)
{
T[, ,] res = new T[x, y, z];
int size = Buffer.ByteLength(array);
Buffer.BlockCopy(array, 0, res, 0, size);
return res;
}
/// <summary>
/// Перевод трехмерного массива в одномерный
/// </summary>
/// <typeparam name="T">Тип исходного массива</typeparam>
/// <param name="array">Исходный массив</param>
/// <returns>Одномерный массив</returns>
public static T[] ToRank1(T[, ,] array, int x, int y, int z)
{
T[] res = new T[x * y * z];
int size = Buffer.ByteLength(array);
Buffer.BlockCopy(array, 0, res, 0, size);
return res;
}
/// <summary>
/// Перевод одномерного массива в четырехмерный
/// </summary>
/// <typeparam name="T">Тип исходного массива</typeparam>
/// <param name="array">Исходный массив</param>
/// <returns>Четырехмерный массив</returns>
public static T[, , ,] Rank1_Rank4(T[] array, int x, int y, int z, int w)
{
T[, , ,] res = new T[x, y, z, w];
int size = Buffer.ByteLength(array);
Buffer.BlockCopy(array, 0, res, 0, size);
return res;
}
/// <summary>
/// Перевод четырехмерного массива в одномерный
/// </summary>
/// <typeparam name="T">Тип исходного массива</typeparam>
/// <param name="array">Исходный массив</param>
/// <returns>Одномерный массив</returns>
public static T[] ToRank1(T[, , ,] array, int x, int y, int z, int w)
{
T[] res = new T[x * y * z * w];
int size = Buffer.ByteLength(array);
Buffer.BlockCopy(array, 0, res, 0, size);
return res;
}
}
}
* This source code was highlighted with Source Code Highlighter.Этот класс обобщенный, что позволило не создавать множество методов для каждого типа отдельно. Смысл этого класса, он повереводит массивы из управляемой памяти в неуправляемую и наоборот. Метод SaveInMem сохраняет структуру, которую мы ему передадим, метод SaveInMem2 сохранит массив, таких как intPtr, Int, float… Ограничение представляет сам метод Marshal.Copy, в котором реализовано копирование отдельно для int, отдельно для byte и некоторых других. Поэтмоу нужно было для каждого типа реализовать свой свой вызов, такой как if (typeof(T) == typeof(int))
Методы ReadInMem и ReadInMem2 для чтения структур и массивов, аналогично SaveMem и SaveMem2.
Как я говорил в первой части статьи, мне приходилось работать с многомерными массивами, но маршалинг не позволяет это делать, он может читать и писать только одномерные массивы. Поэтому я создать подкласс UnArray, который также обобщенный, имеет несколько методов, которые создают массивы из одномерного в двумерный, трехмерный, четырехмерный и наоборот. Правда все эти методы мне нужны были больше в первой части, во второй, при работе с массивами почти не пригодились, хотя можно было бы и несколько методов добавить, тогда пригодились бы.
И так, теперь переходим непосредственно к самому вызову.
Из С, вызов выгладит так:
extern "C" int __import TkzIvc(void *mpGS[]);
* This source code was highlighted with Source Code Highlighter.Для C# этот вызов будет выглядить так:
[DllImport(@"DllTkzIvc.dll")]
private static extern int _TkzIvc([In] IntPtr mpGS);
* This source code was highlighted with Source Code Highlighter.Осталось самое простое. Сделать обертку для использования
[StructLayout(LayoutKind.Sequential)]
public class mpSh_Struct : IDisposable
{
private IntPtr[] mpSh = new IntPtr[5];
private int size_vetv; // число ветвей
/// <summary>
/// Путь к файлам схемы
/// </summary>
/// <value>Строка размером до 255 символов</value>
/// <exception cref="System.ArgumentOutOfRangeException">Возникает, если строка больше, чем 255 символов</exception>
public string PathSh
{
get
{
return Marshal.PtrToStringAnsi(this.mpSh[0]);
}
set
{
if (String.IsNullOrEmpty(value) || value.Length > 255)
throw new ArgumentOutOfRangeException("Данные должны быть обязательно заданы и размером не более 255 символов");
this.mpSh[0] = Marshal.StringToHGlobalAnsi(value);
}
}
///<summary>
/// указатель массива int[]
///</summary>
public int[] TypeV
{
get { return UnMemory<int>.ReadInMem2(this.mpSh[1], this.size_vetv); }
set { UnMemory<int>.SaveInMem2(value, ref this.mpSh[1]); }
}
/// <summary>
/// указатель массива char u1[][6]
/// </summary>
/// <value>Массив из строк размером до 5 символов</value>
public string[] u1
{
get
{
// читаем массив байт из памяти
byte[] mem = UnMemory<byte>.ReadInMem2(this.mpSh[2], this.size_vetv * 6);
// определяем, каким же должен быть результирующий массив string[]
int length = this.size_vetv;
// генерируем новый массив
string[] res = new string[length];
for (int i = 0; i < length; i++)
{
// в цикле обрабатываем массив байт, который переводим в String и обрезаем символы конца
res[i] = Encoding.Default.GetString(mem, i * 6, 6).TrimEnd('\0');
}
return res;
}
set
{
// генерируем массив char[]
char[] res = new char[value.Length * 6];
for (int i = 0; i < value.Length; i++)
{
if (value[i] != null)
value[i].CopyTo(0, res, i * 6, value[i].Length);
}
// сохраняем
UnMemory<char>.SaveInMem2(res, ref this.mpSh[2]);
}
}
/// <summary>
/// указатель массива float[]
/// </summary>
public float[] EK1B1
{
get { return UnMemory<float>.ReadInMem2(this.mpSh[4], this.size_vetv); }
set { UnMemory<float>.SaveInMem2(value, ref this.mpSh[4]); }
}
public int[] ParamSh
{
get { return UnMemory<int>.ReadInMem2(this.mpSh[3], 6); }
set
{
this.size_vetv = value[0]; // число ветвей (ParamSh[0])
UnMemory<int>.SaveInMem2(value, ref this.mpSh[3]);
}
}
/// <summary>
/// Вызов библиотеки на обновление данных
/// </summary>
public bool Read(out string errorText)
{
try
{
IntPtr t = new IntPtr();
UnMemory<IntPtr>.SaveInMem2(this.mpSh, ref t);
_TkzIvc(t);
mpSh = UnMemory<IntPtr>.ReadInMem2(t, this.mpSh.Length);
int[] paramSh = this.ParamSh; // параметры схемы
this.size_vetv = paramSh[0]; // число ветвей
errorText = String.Empty;
return true;
}
catch (DllNotFoundException)
{
errorText = "Целость программы нарушена. Пожалуйста переустановите программу или обратитесь к администратору за помощью";
return false;
}
catch (Exception exp)
{
errorText = exp.Message;
return false;
}
}
public void Dispose()
{
// очищаем память для выделенных объектов
UnMemory.FreeMemory();
// очищаем то, что выделялось библиотечкой
for (int i = 1; i < 5; i++)
{
IntPtr ptr = mpSh[i];
// чистим
UnMemory.FreeIntPtr(ptr);
}
}
}
* This source code was highlighted with Source Code Highlighter.ОБРАТИТЕ ВНИМАНИЕ, что для простого текста, строки, я использовал Marshal.StringToHGlobalAnsi(text). Т.к. это специальный метод класса Marshal для чтения и записи обычной текстовой строки, которая оканчивается символом '\0'
Массив указателей сохраняется в mpSh, а для всех полей реализованы методы get и set, которые почти прозрачно от самого класса пишут и читают неуправляемую память. Метод Read переносит массив указателей в неуправляемую память, чтобы потом прочесть данные из неё (при вызове библиотеки, эти данные заполняются). После вызова Read, мы сможем прочесть нужную нам переменную просто обратившись к нужному полю.
Правда есть одно ограничение… лучше получить ссылку на массив ( int[] paramSh = this.ParamSh ), а уже потом работать с ним, чем обращаться к полю постоянно ( this.size_vetv = this.ParamSh[0] ). Потому как при обращении, данные заново будут читаться из неуправляемой памяти. Несмотря, что это проиходит быстро, если вызовов много, это может занять достаточное время.
PS: в рамках данной статьи не был рассмотрен CustomMarshaling. Суть его в том, что Вы можете польностью настраивать, как должна сохраняться определенная структура, с его помощью Вы можете поддерживать различные версии своих библиотек. Например, сохранять DateTime как String в определенном формате и пр. Такие случаи бывают реже, поэтому этому можно посветить отдельную статью.
К сожалению приложить исходники к статье не могу, т.к. это не одобрит руководство :) Но я постарался привести максимум кода, чтобы без исходников было понятно, как работать с маршалингом. Если вам будем что-то непонятно, пожалуйста напишите и я отвечу на ваши вопросы.