Данная справка рассказывает про подводные камни при работе с неуправляемыми .dll,
которые можно встретить при работе с модулями OpenGL
и OpenCL
, входящеми в состав стандартных модулей языка PascalABC.NET.
Справки по соответствующим библиотекам:
(Далее "исходные библиотеки" относится сразу к обеим этим библиотекам)
Отдельных справок по модулям OpenGL
и OpenCL
(далее просто "н.у. модули", что значит "низко-уровневые")
нет, потому что они сделаны по
общим принципам
.
Н.у. модули созданы как промежуточная точка между исходными библиотеками и модулями
OpenGLABC
и
OpenCLABC
,
на случай если вы хотите использовать OpenGL и/или OpenCL, но хотите написать свою высоко-уровневую оболочку.
Возможно, ради опыта. Возможно, ради особых оптимизаций. И т.п.
Если вы будете использовать только модули OpenGLABC
и OpenCLABC
- данная справка вам не нужна,
потому что они специально созданы так, чтобы отстранить программиста от всех сложностей
работы с неуправляемыми .dll и предоставить привычный ООП интерфейс со всевозможными удобствами.
Если:
В н.у. модулях или данной справке найдена ошибка или чего-либо не хватает;
Вы наткнулись на какую то особенностью работы с неуправляемыми .dll, которая тут не (или недо-) описана:
Пишите в issue.
Лучше прочитайте оффициальный документ от microsoft, если хотите точной и подробной информации.
Если в кратце:
Есть платформа .Net, объединяющая много языков программирования. В том числе C# и PascalABC.NET.
.exe и .dll созданные такими языками содержат код в особом виде, позволяющем легко подключать
.dll (и теоретически .exe, но это плохо) созданные на 1 .Net языке к программе на любом другом .Net языке.
(в паскале это делается с помощью $reference
)
Такие .exe и .dll называются управляемыми. .exe и .dll созданные на любом другом (то есть не .Net) языке называются неуправляемыми.
OpenCL.dll и OpenGL.dll созданы на- и для языков C/C++, поэтому являются неуправляемыми.
Большинство функций из н.у. модулей требует какой-либо чистки. В основном потому что
при переводе с C++ тип int*
может оказаться и указателем, и var
-параметром, и массивом.
При автоматическом переводе кода с C++ на паскаль - создаются все возможные перегрузки. А те перегрузки, которые не подходят конкретным подпрограммам - надо убирать вручную. В этом и состоит чистка подпрограмм.
Все разом функции не почистишь, в одном только OpenGL
их >3000. Но сделать это, всё же, надо.
Если хотите помочь - можно писать и в issue, но лучше (и проще для вас, когда разберётесь) создать fork репозитория,
сделать там нужные изменения и послать pull-request. Могу объяснить подробнее в vk или по sunserega2@gmail.com
.
Чтобы чистить подпрограммы было проще - я написал несколько инструментов.
Вся упаковка н.у. модулей находится тут.
Откройте папку OpenCL
или OpenGL
. Они устроены одинаково, поэтому объяснять можно на любой из них.
Все файлы (кроме .exe, .pdb и .pcu, которые создаёт паскаль) в этих папках открываются любым текстовым редактором.
Контроль содержимого модуля находится в папке Fixers
. В этой папке есть следующие подпапки:
Funcs
- контроль подпрограмм;Enums
- контроль записей-перечислений;Structs
- контроль просто записей (записей-контейнеров, если хотите).Во всех папках находится файл ReadMe.md
, объясняющий синтаксис.
MiscInput
содержит другие данные, используемые при создании модулей, которые имеют разные значения для OpenCL и OpenGL.
Синтаксис у всех предельно прост. Не вижу смысла объяснять подробнее.
Log
содержит логи последней сборки соответствующего модуля.
Они в основном используются чтобы было проще увидеть на что именно повлияли ваши изменения и как (используя проверку изменений git'а).
Но файл FinalFuncOverloads.log
так же особо полезен перед
началом чистки, чтобы увидеть какие перегрузки уже есть.
Чтобы применить фиксеры и посмотреть на что они влияют - вызывайте Pack Most.pas
.
Ну а чтобы полностью собрать модули - вызывайте PackAll.exe
в корне репозитория.
(или .bat
файлы там же, для сборки отдельных компонентов)
Если нужна высоко-уровневость - используйте соответственно модули OpenGLABC и OpenCLABC .
В исходных библиотеках обращение ко всем объектам идёт по "именам" (их так же можно назвать дескрипторами или id этих объектов).
Имена объектов - это числа (в OpenGL обычно на 32 бита, в OpenCL - зависит от битности системы).
Чтобы в подпрограмму, принимающую имена объектов определённого типа нельзя было передать имя объекта неправильного типа - в н.у. модулях для каждого типа объектов описана подобная запись:
gl_buffer = record
public val: UInt32;
public constructor(val: UInt32) := self.val := val;
public static property Zero: gl_buffer read default(gl_buffer);
public static property Size: integer read Marshal.SizeOf&<UInt32>;
public function ToString: string; override := $'gl_buffer[{val}]';
end;
Такой подход не замедляет готовую программу, но позволяет отловить некоторые ошибки на этапе компиляции.
Поле .val
и конструктор публичны только на случай ошибки в перегрузках,
то есть если подпрограмма принемает неправильный тип имени.
В обычной ситуации - вы будете взаимодействовать с именами только 3 способами:
Объявление:
var name: gl_buffer;
var names: array of gl_buffer := new gl_buffer[5];
Передача исходным библиотекам:
gl.CreateBuffers(1, name);
gl.CreateBuffers(names.Length, names);
Использование статического свойства .Zero
:
procedure MyProc(buff: gl_buffer);
begin
...
end;
...
MyProc(gl_buffer.Zero);
// То же самое, но с лишними скобками:
MyProc(default(gl_buffer));
У настоящих объектов имя никогда не будет нулевым.
Но бывает не мало случаев когда исходные библиотеки могут принимать нулевое имя.
К примеру, привязка шейдера с нулевым именем в OpenGL отменяет предыдущую привязку шейдера.
Многие параметры подпрограмм в исходных библиотеках принимают перечисления (enum'ы).
Перечисления, как и имена, это числа. Но в отличии от имён - перечисления принимают заданные заранее константные значения.
В качестве примера:
procedure gl.BeginQuery(target: QueryTarget; id: gl_query)
Параметр target
принимает одно из значений, сгрупированных в записи QueryTarget
:
QueryTarget = record
public val: UInt32;
public constructor(val: UInt32) := self.val := val;
public static property TRANSFORM_FEEDBACK_OVERFLOW: QueryTarget read new QueryTarget($82EC);
public static property VERTICES_SUBMITTED: QueryTarget read new QueryTarget($82EE);
public static property PRIMITIVES_SUBMITTED: QueryTarget read new QueryTarget($82EF);
public static property VERTEX_SHADER_INVOCATIONS: QueryTarget read new QueryTarget($82F0);
public static property TIME_ELAPSED: QueryTarget read new QueryTarget($88BF);
public static property SAMPLES_PASSED: QueryTarget read new QueryTarget($8914);
public static property ANY_SAMPLES_PASSED: QueryTarget read new QueryTarget($8C2F);
public static property PRIMITIVES_GENERATED: QueryTarget read new QueryTarget($8C87);
public static property TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN: QueryTarget read new QueryTarget($8C88);
public static property ANY_SAMPLES_PASSED_CONSERVATIVE: QueryTarget read new QueryTarget($8D6A);
...
end;
То есть вызов может выглядеть так:
var name: gl_query;
...
gl.BeginQuery(QueryTarget.VERTICES_SUBMITTED, name);
Чтобы увидеть доступные значения - достаточно написать QueryTarget
и поставить точку,
после этого анализатор кода покажет список доступных имён.
Так же бывают особые перечисления - битовые маски. Они могут принимать сразу несколько значений:
procedure gl.Clear(mask: ClearBufferMask);
gl.Clear(
ClearBufferMask.COLOR_BUFFER_BIT + // Очистка поверхности рисования одним цветом
ClearBufferMask.DEPTH_BUFFER_BIT // Очистка буфера глубины - нужна при рисовании 3D
);
Все подпрограммы исходных библиотек можно разделить на подпрограммы ядра + множество мелких расширений.
Все подпрограммы ядра находятся в классе gl
/cl
для OpenGL/OpenCL соответственно.
То есть если вы хотите вызвать функцию clCreateBuffer
надо писать:
cl.CreateBuffer(...);
По нажатию точки после cl
вам так же покажет список всех функций в ядре OpenCL.
С OpenGL немного сложнее:
В модуле OpenGL
это реализовано так:
//ToDo Создание контекста и привязка его к текущему потоку
// Все адреса и экземпляры делегатов создаются в конструкторе
// В "<>" указывается имя платформы
// Можете написать "Pl" и нажать Ctrl+пробел, чтобы получить список платформ
var gl := new OpenGL.gl<PlWin>;
while true do
begin
// Локальные переменные имеют бОльший приоритет чем классы
// Поэтому тут "gl" это не класс, а экземпляр, который создали выше
gl.Clear( ClearBufferMask.COLOR_BUFFER_BIT );
//ToDo само рисование
end;
У каждого расширения есть свой класс. К примеру, так используется расширение GL_AMD_debug_output
:
//ToDo Опять же, сначала контекст
var glDebugOutputAMD := new OpenGL.glDebugOutputAMD<PlWin>;
...
glDebugOutputAMD.DebugMessageEnableAMD(...);
В модуле OpenGL
так же есть особые классы, wgl
, gdi
и glx
:
gdi
содержит несколько методов библиотеки gdi32.dll
. На этой библиотеке основано всё в System.Windows.Forms
.
Подпрограммы включённые в класс gdi
- это то, что может понадобиться вам, чтобы настроить и подготовить форму для рисования на ней с помощью OpenGL.
wgl
содержит методы для подключения OpenGL к окну Windows.
glx
содержит методы для подключения OpenGL к окну XWindow.
Все эти классы работают как класс cl
, то есть им не надо создавать экземпляр.
И последнее что вам надо знать об этих классах:
Если вы получаете NullReferenceException
,
при попытке вызова функции из инициализируемых классов, как gl
:
Скорее всего вы попытались вызвать подпрограмму, которой нет в реализации библиотеки на вашем компьютере.
Проверьте версию библиотеки, или, если это подпрограмма из расширения - проверьте существование у вас этого расширения.
Так же, возможно, проблема в н.у. модуле. Если вы уверены что п.1. вас не касается - напишите в issue.
Библиотека OpenGL.dll
имеет несколько функций, принимающих
вектора и матрицы (в основном для передачи значений в шейдеры).
В модуле OpenGL
для каждого типа вектора и матрицы описана отдельная запись.
Они особенны тем, что поддерживают некоторые математические операции, которые можно считать
высокоуровневыми, а значит противоречущими основным принципам н.у. модулей.
Но реализовывать их все в качестве extensionmethod
'ов было бы сложно, не красиво,
а в случае статических методов и свойств - ещё и невозможно.
ToDo
сейчас все индексные свойства кроме .ColPtr
(.val
, .Row
и .Col
) убраны из релиза,
потому что я не знаю как безопасно и эффективно их реализовывать. Постараюсь в ближайшее
время придумать, что можно сделать.
Все типы векторов можно описать разом как Vec[1,2,3,4][ b,ub, s,us, i,ui, i64,ui64, f,d ]
.
Каждый тип вектора берёт по 1 из значений, перечисленных в []
, через запятую.
К примеру, есть типы Vec2d
и Vec4ui64
.
Число в первых скобках - значит кол-во измерений вектора.
Буква (буквы) в следующих скобках - значат тип координат вектора:
b=shortint
, s=smallint
, i=integer
, i64=int64
: Все 4 типа - целые числа, имеющие бит знака (±) и занимающие 1, 2, 4 и 8 байт соответственно;
Они же но с приставкой u
- целые числа без знака. К примеру ui
значит целое на 4 байта без знака, то есть longword
(он же cardinal
);
f=single
и d=real
- числа с плавающей запятой, на 4 и 8 байт соответственно.
Таким образом Vec2d
хранит 2 числа типа real
, а Vec4ui64
хранит 4 числа типа uint64
.
У векторов есть только индексное свойство val
. Оно принимает индекс, считаемый от 0,
и возвращает или задаёт значение вектора для соответствующего измерения.
К примеру:
var v: Vec4d;
v[0] := 123.456; // Записываем 123.456 по индексу 0
v[1].Println; // Читаем и выводим значение по индексу 1
v.val[2] := 1; // Можно так же писать и имя свойства
Но использование этого свойства не рекомендуется. Прямое обращение к полю всегда будет быстрее. То есть аналогично предыдущему коду:
var v: Vec4d;
v.val0 := 123.456;
v.val1.Println;
v.val2 := 1;
Используйте свойство val
только тогда, когда индекс это НЕ константа.
var v0: Vec3d;
...
// v1 будет иметь ту же длину, но
// противоположное v0 направление
var v1 := -v0;
// А унарный + не делает ничего, он только
// для красоты. То есть v2=v0 тут
var v2 := +v0;
var v1: Vec3d;
var v2: Vec3i;
...
// Выведет вектор, имеющий то же
// направление что v1, но в 2 раза длиннее
(v1*2).Println;
// Выведет вектор, имеющий то же
// направление что v1, но в 2 раза короче
(v1/2).Println;
// К целочисленным векторам вместо
// обычного деления надо применять div
(v2 div 2).Println;
var v1, v2: Vec3d;
...
// Скалярное произведение векторов
(v1*v2).Println;
// Сумма векторов, складывает
// отдельно каждый элемент вектора
(v1+v2).Println;
// Разность векторов, тоже работает
// отдельно на каждый элемент вектора
(v1-v2).Println;
Чтобы применить 1 из этих операций к 2 векторам - их типы должны быть одинаковые.
Если это не так - 1 из них (или оба) надо явно преобразовать, так чтобы типы были одинаковые:
var v1: Vec3d;
var v2: Vec2i;
...
( v1 + Vec3d(v2) ).Println;
Метод .SqrLength
возвращает квадрат длины (то есть модуля) вектора.
Возвращаемый тип .SqrLength
совпадает с типом элементов вектора.
Каким образом находить корень полученного значения - дело программиста.
var v1: Vec3d;
...
v1.SqrLength.Println; // Квадрат длины
v1.SqrLength.Sqrt.Println; // Сама длина
Метод .Normalized
возвращает нормализированную (с длиной =1) версию вектора.
Так как эта операция требует деления (на длину вектора), она применима только
к векторам с элементами типов single
или real
(f
или d
).
var v1 := new Vec3d(1,1,1);
v1.Println;
v1.SqrLength.Sqrt.Println;
var v2 := v1.Normalized;
v2.Println;
v2.SqrLength.Sqrt.Println; // Обязательно будет 1
Статичные методы .Cross[CW,CCW]
возвращают векторное произведение двух
3-х мерных векторов ("Cross product", не путать со скалярным произведением).
Векторное произведение - это вектор, перпендикулярный обоим входным векторам и имеющий длину,
равную площади параллелограмма, образованного входными векторами.
Не работает для векторов с элементами-беззнаковыми_целыми, потому что даёт переполнение на практически любых входных значениях. Если найдёте нормальное применение - напишите в issue.
В математике произведение векторов может вернуть один из двух противоположных друг-другу векторов,
в зависимости от ориентации системы координат. В модуле OpenGL
это решено следующим образом:
CW (Clockwise - по часовой стрелке):
Vec3d.CrossCW(new Vec3d(1,0,0), new Vec3d(0,1,0)) = new Vec3d(0,0,1)
.
CСW (Counter-Clockwise - против часовой стрелки):
Vec3d.CrossCCW(a,b) = -Vec3d.CrossCW(a,b) = Vec3d.CrossCW(b,a)
;
Кроме этого, статические методы .Cross[CW,CCW]
так же объявленны для 2D векторов.
Для них результат является z-компонентом соответствующего метода для 3D векторов:
Vec2d.CrossCW(a,b) = new Vec3d(0,0,1) * Vec3d.CrossCW(Vec3d(a),Vec3d(b))
С другом стороны, 2D векторное произведение это определитель матрицы (не важно, по строкам или столбцам):
Vec2d.CrossCW(a,b) = Mtr2d.FromCols(a,b).Determinant
Статичный метод .Random
создаёт новый вектор из случайных значений в заданном диапазоне:
// Вектор будет иметь значения из [0;1)
Vec2d.Random(0,1).Println;
Статичные методы Read
и Readln
создают новый вектор из элементов, прочитанных из стандартного ввода:
// Прочитать 2 числа из ввода
Vec2d.Read('Введите 2 координаты:').Println;
// Прочитать 2 числа из ввода
// и затем пропустить всё до конца строки
Vec2d.Readln.Println;
var v1: Vec4d;
...
v1.Println; // Вывод вектора
// s присвоит ту же строку, что выводит .Println
var s := v1.ToString;
Методы .ToString
и .Println
должны быть использованы
только для чего то вроде дебага или красивого вывода,
потому что операции со строками это в целом медленно.
Все типы матриц можно описать разом как Mtr[2,3,4]x[2,3,4][f,d]
.
У каждой квадратной матрицы есть короткий синоним.
К примеру вместо Mtr3x3d
можно писать Mtr3d
.
Так же стоит заметить - конструктор матрицы принимает элементы по строкам, но в самой матрице элементы хранятся в транспонированном виде.
Это потому, что в OpenGL.dll
в шейдерах матрицы хранятся по столбцам.
Но если создавать матрицу конструктором - элементы удобнее передавать по строкам, вот так:
var m := new Mtr3d(
1,2,3, // (1;2;3) станет нулевой строкой матрицы
4,5,6,
7,8,9
);
Как и у векторов, у матриц есть свойство val
:
var m: Mtr4d;
m[0,0] := 123.456;
m[1,2].Println;
m.val[3,1] := 1;
И как и у векторов - val
всегда медленнее прямого обращения к полям:
var m: Mtr4d;
m.val00 := 123.456;
m.val12.Println;
m.val31 := 1;
Но у матриц так же есть свойства для столбцов и строк:
var m: Mtr3d;
...
m.Row0.Println; // Вывод нулевой строчки в виде вектора
m.Row1 := new Vec3d(1,2,3);
m.Col2.Println;
И в качестве аналога val
- строку и стобец тоже можно
получать по динамическому индексу (но, опять же, это медленнее):
var m: Mtr3d;
...
m.Row[0].Println;
m.Row[1] := new Vec3d(1,2,3);
m.Col[2].Println;
Для столбцов так же есть особые свойства, возвращающие не столбец, а его адрес в памяти:
var m: Mtr3d;
...
var ptr1 := m.ColPtr0;
var ptr2 := m.ColPtr[3];
Использовать это свойство не всегда безопасно.
Оно должно быть использовано только для записей, хранящихся на стеке или в неуправляемой памяти.
Для более безопасной альтернативы - можно использовать методы .UseColPtr*
.
Это тоже свойство, но статическое и применение совершенно другое:
Identity
возвращает новую единичную матрицу. То есть матрицу, у
которой главная диагональ заполнена 1, а всё останое заполнено 0.
Mtr3d.Identity.Println;
// Работает и для не_квадратных матриц
Mtr2x3d.Identity.Println;
Методы .UseColPtr*
принимают подпрограмму, принимающую
адрес определённого столбца в виде var
-параметра.
В отличии от свойств .ColPtr*
, методы .UseColPtr*
безопасны для
матриц, хранящихся в экземплярах классов и статических полях:
uses OpenGL;
procedure p1(var v: Vec3d);
begin
Writeln(v);
end;
function f1(var v: Vec3d): string :=
v.ToString;
begin
var o := new class(
m := Mtr3d.Identity
);
o.m.UseColPtr0(p1);
o.m.UseColPtr1(f1).Println;
end.
Статичный метод .Scale
возвращает матрицу, при
умножении на которую вектор маштабируется в k раз.
var m := Mtr3d.Scale(2);
var v := new Vec3d(1,2,3);
(m*v).Println; // (2;4;6)
Статичный метод .Translate
возвращает матрицу, при
умножении на которую к вектору добавляется заданное значение.
var m := Mtr4d.Translate(1,2,3);
// Последний элемент должен быть 1,
// чтобы матрица из .Translate правильно работала
var v := new Vec4d(0,0,0,1);
(m*v).Println; // (1;2;3)
Так же есть статический метод .TraslateTransposed
. Он возвращает
ту же матрицу что .Translate
, но в транспонированном виде.
Группа статических методов .Rotate[XY,YZ,ZX][cw,ccw]
возвращает матрицу вращения в определённой плоскости.
Первые скобки определяют плоскость.
(Но у 2x2 матриц есть только XY вариант)
Вторые скобки определяют направление вращения:
Группа статических методов .Rotate3D[cw,ccw]
возвращает матрицу
вращения вокруг нормализованного 3-х мерного вектора.
(разумеется, не существует для матриц 2x2,2x3 и 3x2)
Метод .Det
возвращает определитель матрицы. Существует только для квадратных матриц.
Метод .Transpose
возвращает транспонированную версию матрицы:
var m := new Mtr2x3d(
1,2,3,
4,5,6
);
m.Transpose.Println; // Выводит:
// 1 4
// 2 5
// 3 6
m*v
- это обычное математическое умножение матрицы m
и вектора v
,
возвращающее результат после применения преобразования из m
к v
.
Но так же как в шейдерах - поддерживается и обратная запись:
v*m
это то же самое что m.Transpose*v
.
m1*m2
- это математическое умножение матриц m1
и m2
.
Статичный метод .Random
создаёт новую матрицу из случайных значений в заданном диапазоне:
// Матрица будет иметь значения из [0;1)
Mtr3d.Random(0,1).Println;
Статичные методы Read[,ln][Rows,Cols]
создают новую матрицу из элементов, прочитанных из стандартного ввода:
// Прочитать 3*4=12 элементов из ввода
// и сохранить в новую матрицу по строкам
Mtr3x4d.ReadRows('Введите 12 элементов матрицы:').Println;
// Прочитать 4 элемета из ввода, переходя на
// следущую строку ввода после чтения каждого столбца
Mtr2d.ReadlnCols(
'Введите столбцы матрицы...',
col -> $'Столбец #{col}:'
).Println;
Как и у векторов - матрицы можно выводить и превращать в строку
var m: Mtr4d;
...
m.Println; // Вывод матрицы
// s присвоит ту же строку, что выводит .Println
var s := m.ToString;
Для того чтобы матрица выведенная 1 из этих методов выглядела красиво надо использовать моноширный шрифт и поддерживать юникод (потому что для матриц используются символы псевдографики).
Обычно это не проблема для .Println
, потому что и консоль, и окно вывода в IDE имеют моноширный шрифт и поддерживают юникод.
Но если выводить на форму, то придётся специально поставить моноширный шрифт.
А если выводить в файл, надо выбрать кодировку файла - юникод (UTF).
Большинство проблем при использовании неуправляемых .dll вытекают из следующих 2 различий:
В .Net строки и массивы это стандартные типы, доступные для всех .Net языков.
А в C++ строки и массивы не только описаны не так же как в .Net . У них так же есть множество разных стандартов. Благо, исходные библиотеки придерживаются одного общего стандарта.
В управляемом коде оперативная память (дальше просто память) обычно управляется сборщиком мусора. Поэтому если создать массив - когда он стал не нужен о нём можно просто забыть.
А в C++ память управляется только программистом, поэтому неуправляемую память нужно всегда освобождать после использования.
Забытая и не_освобождённая неуправляемая память называется утечкой памяти. И это один из самых сложно-ловимых багов,
потому что у него нет явно видимых симптомов, вроде вызова ошибки, пока память окончательно не закончится.
В .Net так же можно выделять и освобождать неуправляемую память, статическими методами класса Marshal
:
.AllocHGlobal
и .FreeHGlobal
соответственно. Обычно это надо для п.1., для преобразований между
управляемыми и неуправляемыми типами.
Ещё одно отличие неуправляемой памяти - она не очищается нулями при выделении, а значит содержит мусорные данные. И память GPU (к примеру, содержимое буферов) тоже.
Полное имя класса Marshal
это System.Runtime.InteropServices.Marshal
.
Чтобы не писать его целиком - можно написать в начале файла uses System.Runtime.InteropServices
, и дальше писать только Marshal
.
Стоит так же заметить, что в н.у. модулях все подпрограммы, напрямую вызывающие подпрограммы из неуправляемых .dll принимают параметрами как неуправляемые, так и управляемые типы массивов и строк, по мере необходимости.
Если ваша необходимость не удовлетворена (то есть не хватает перегрузки с определённым типом) - это особо хороший повод написать в issue.
Кроме удаления неиспользуемых экземпляров классов, сборщик мусора так же может произвольно перемещать используемые объекты, более плотно упаковая их в памяти.
И он прекрасно справляется с тем, чтобы сделать эти перемещения незаметными, в обычных ситуациях. Но как только речь находит об указателях и неуправляемом коде - начинаются проблемы. Чтобы избежать их, надо очень хорошо понимать как работает сборщик мусора.
uses System;
uses System.Runtime.InteropServices;
// Отдельный разбор:
// http://forum.mmcs.sfedu.ru/t/testy-raboty-sborshhika-musora
function get_addr(a: array of integer) := '$'+Marshal.UnsafeAddrOfPinnedArrayElement(a,0).ToString('X');
// Заполняем память кучей разных мусорных объектов,
// которые никогда не удалятся, чтобы
// сборщик мусора засуетился когда его тыкнут
// Не знаю почему надо именно массив массивов + массив их адресов
var ___a := ArrGen(10000, i->|0|);
var ___p := ___a.ConvertAll(get_addr);
type
punch_gc_callback = procedure(ptr: pointer);
function ptr_adr<T>(var a: T) := new IntPtr(@a);
{$region Safe}
function copy_arr(var a: integer; punch_gc: punch_gc_callback): IntPtr;
external 'Native\x64\Release\Native.dll';
function copy_arr(var a: byte; punch_gc: punch_gc_callback): IntPtr;
external 'Native\x64\Release\Native.dll';
function copy_arr([MarshalAs(UnmanagedType.LPArray)] a: array of integer; punch_gc: punch_gc_callback): IntPtr;
external 'Native\x64\Release\Native.dll';
function copy_arr_recall2(a: array of integer; punch_gc: punch_gc_callback): IntPtr :=
copy_arr(a, punch_gc);
function copy_arr_recall3(var a: integer; punch_gc: punch_gc_callback): IntPtr :=
copy_arr(a, punch_gc);
function copy_arr_recall5(var a: integer; punch_gc: punch_gc_callback): IntPtr :=
copy_arr(PByte(pointer(@a))^, punch_gc);
{$endregion Safe}
{$region Unsafe}
function copy_arr(a: IntPtr; punch_gc: punch_gc_callback): IntPtr;
external 'Native\x64\Release\Native.dll';
function copy_arr(a: ^integer; punch_gc: punch_gc_callback): IntPtr;
external 'Native\x64\Release\Native.dll';
function copy_arr_recall1(var a: integer; punch_gc: punch_gc_callback): IntPtr :=
copy_arr(@a, punch_gc);
function copy_arr_recall4_helper(a: ^integer; punch_gc: punch_gc_callback): IntPtr;
begin
punch_gc(a);
Result := copy_arr(a^, ptr->begin end); // Второй раз вызывать punch_gc и вывод - ни к чему, всё ломается уже на предыдущей строчке
end;
function copy_arr_recall4(var a: integer; punch_gc: punch_gc_callback): IntPtr :=
copy_arr_recall4_helper(@a, punch_gc);
function get_int(punch_gc: punch_gc_callback; var a: integer): integer;
begin
punch_gc(@a);
Result := 4;
end;
function copy_arr_recall6(var a: integer; punch_gc: punch_gc_callback): IntPtr :=
copy_arr(PByte(pointer(IntPtr(pointer(@a))+get_int(punch_gc, a)))^, ptr->begin end);
{$endregion Unsafe}
procedure punch_gc := GC.Collect;
begin
var a := Arr(1,2,3,4,5,6);
var b := Arr(1,2,3,4,5);
Console.WriteLine('begin');
Console.WriteLine(get_addr(a));
Console.WriteLine(get_addr(b));
// punch_gc работает только 1 раз, эти строчки только чтобы протестировать, работает ли он у вас вообще
// punch_gc;
// Console.WriteLine('after first gc');
// Console.WriteLine(get_addr(a));
// Console.WriteLine(get_addr(b));
{$region заголовки вызова copy_arr}
// Безопасно
// var ptr := copy_arr(a, // Передавать как массив безопасно
// var ptr := copy_arr(a[0], // Передавать элемент массива var-параметром безопасно
// var ptr := copy_arr(a[1], // И это касается не только элемента [0]
// var ptr := copy_arr_recall2(a, // Безопасно, потому что с точки зрения copy_arr_recall2 ситуация та же что "copy_arr(a,"
// var ptr := copy_arr_recall3(a[0], // И var-параметры тоже безопасны через промежуточные подпрограммы
// var ptr := copy_arr_recall5(a[0], // Тут указатели не попадают в готовый .exe, они только чтобы успокоить компилятор, поэтому безопасно
// НЕ безопасно
// var ptr := copy_arr(Marshal.UnsafeAddrOfPinnedArrayElement(a,0), // GC не следит за содержимым IntPtr
// var ptr := copy_arr(ptr_adr(a[0]), // И за другими формами указателей тоже
// var ptr := copy_arr_recall1(a[0], // Проблема не в передаче адреса возвращаемым значением из ptr_adr в copy_arr
// var ptr := copy_arr_recall4(a[0], // Кроме того, проблема вообще не в неуправляемом коде, в управляемом тоже воспроизводится
// var ptr := copy_arr_recall6(a[0], // В отличии от recall5 - тут указатели попадают в готовый .exe, поэтому небезопасно
{$endregion заголовки вызова copy_arr}
ptr->
begin
Console.WriteLine('before gc');
Console.WriteLine(get_addr(a));
Console.WriteLine('$'+IntPtr(ptr).ToString('X'));
Console.WriteLine(get_addr(b));
// "b" используется только чтобы видеть когда punch_gc успешно сработал
// (хотя всегда может переместить только "a", или только "b", поэтому не 100% показатель)
// Но главное тут - "ptr" это то, что хранит неуправляемый код, а "get_addr(a)" показывает текущий адрес "a"
// Если "get_addr(a)" изменился после вызова "punch_gc", а "ptr" остался тем же - значит "a" не заблокирован
punch_gc;
Console.WriteLine('after gc');
Console.WriteLine(get_addr(a));
Console.WriteLine('$'+IntPtr(ptr).ToString('X'));
Console.WriteLine(get_addr(b));
end);
Console.WriteLine('end');
Console.WriteLine(get_addr(a));
Console.WriteLine(get_addr(b));
// punch_gc;
// Console.WriteLine('after last gc');
// Console.WriteLine(get_addr(a));
// Console.WriteLine(get_addr(b));
// Показывает эффекты НЕ безопасного вызова
// Точнее если неуправляемый код потеряет адрес массива, то тут
// может быть мусор или ошибка доступа, правда я их ни разу не получил
var res := new byte[20];
Marshal.Copy(ptr,res,0,20);
res.Println;
end.
Dll1.dll
должна быть неуправляемой библиотекой, содержащей следующую функцию (это C++):
extern "C" __declspec(dllexport) BYTE* copy_arr(int* a, void (*punch_gc)(void*))
{
BYTE* res = new BYTE[20]; // Выделяем 20 байт неуправляемой памяти
punch_gc(a); // Вызываем ту подпрограмму, чей адрес сюда передали
memcpy(res, a, 20); // Копируем 20 байт из "a" в "res"
return res; // Плохо что неуправляемая память не освобождается, но в этом тесте не важно
}
Подробнее о параметрах:
a
принимает указатель на integer
, что в C++ так же может являеться массивом с элементами типа integer
;
punch_gc
принемает адрес подпрограммы, принемающей void*
(безтиповый указатель)
и возвращающей void
(ничего не возвращающей, то есть это процедура);
Ну и возвращаемое значение - BYTE*
. Так же как a
, вообще указатель, но в данном случае массив.
Почитать более подробный разбор и скачать архив с упакованной библиотекой можно тут.
Пожалуйста, попробуйте поэксперементировать с этим кодом сами. И если найдёте что-то интересное - обязательно напишите в issue. В этом деле много тестов не бывает.
Вся безопасность зависит только от объявления параметра external
-подпрограммы,
через который передаётся управляемый объект. Если параметр принимает:
Любой размерный тип (то есть запись):
При передаче в подпрограмму размерное значение копируется.
Сборщик мусора тут не при чём. Поэтому передача записи всегда безопасна.
Массив или var
-параметр:
Пока вызов неуправляемой подпрограммы не завершится - сборщик мусора НЕ будет передвигать объект в памяти.
Но обратите внимание, есть неуправляемые подпрограммы, использующие переданные вами данные после завершения вызова external
-подпрограммы.
К примеру gl.Uniform1iv
скопирует данные из вашего массива в память GPU, поэтому он безопасен.
Но cl.CreateBuffer
в который передали MemFlags.MEM_USE_HOST_PTR
создаст буфер, который
будет ссылаться на ваш массив. В таком случае надо использовать GCHandle
с GCHandleType.Pinned
, чтобы массив не двигался в памяти.
Поэтому очень внимательно читайте документацию того, что вызываете.
Указатель в любом виде (типизированный, безтиповый или даже обёрнутый в запись вроде IntPtr
):
Передавать адрес содержимого класса НЕ безопасно.
Можно передавать только адрес локальной переменной, которая не была захвачена лямбдой.
Ну или адрес заведомо неуправляемого куска памяти, к примеру выделенного с помощью Marshal.AllocHGlobal
.
Так же протестировал mono
(версия .Net для линукса) на arch-linux и Windows10.
Среда выполнения mono
блокирует управляемые объекта не зависимо от типа параметра,
даже если external
подпрограмма принимает указатель или IntPtr
.
То есть при запуске из под mono
волноваться о блокировке объектов надо только в случае,
если неуправляемый код будет использовать данные этого объекта после выхода из вызова external
-подпрограммы.
Ну и нельзя забывать о случае, когда данные объекта имеют сложный тип, требующий маршлинга перед передачей в неуправляемый код. Подробнее на странице ниже.
В .Net var
-параметры реализованы через указатели. То есть эти 2 кода:
procedure p1(var i: integer);
begin
i.Println;
i := 5;
end;
begin
var i := 3;
p1(i);
i.Println;
end.
procedure p1(i: ^integer);
begin
i^.Println;
i^ := 5;
end;
begin
var i := 3;
p1(@i);
i.Println;
end.
Генерируют практически одинаковые .exe .
Отличие состоит в том, что передавать содержимое класса (в том числе массива) указателем опасно - сборщик мусора может в любой момент переместить память, ломая указатель.
В то же время если передавать это же значение var
-параметром - сборщик мусора
поправит адрес при перемещении памяти.
Статические поля так же могут быть перемещены, как и содержимое экземпляров, даже если они имеют размерный тип.
И глобальные переменные это, на самом деле, тоже статические поля.
А захваченные лямбдой локальные переменные превращаются в поля специального анонимного класса.
В .Net массивы хранят не только содержимое, но и данные о своём размере.
А в C++ вместо обычных массивов используется безформенная область памяти.
При её выделении - в переменную записывается указатель [0]
элемента.
А о том чтобы сохранить данные о размере этой области - должен позаботится программист.
(на самом деле обычно в C++ используют обёртки, хранящие длину так же как .Net массивы. Но OpenGL.dll и OpenCL.dll это не касается)
Если вы видели старые коды с использованием OpenGL из какого то-из паскалей - наверняка видели что то такое:
glИмяПодпрограммы(@a[0]);
Но в PascalABC.Net так делать нельзя! Получение указателя на элемент массива моментально создаёт утечку памяти,
потому что компилятор, на всякий случай, вставляет полную блокировку массива в памяти, используя GCHandle
с GCHandleType.Pinned
.
Такая блокировка нужна, потому что иначе полученный указатель может в любой момент стать устаревшим.
Обычно GCHandle
освобождают методом .Free
. Но если позволить компилятору использовать
GCHandle
- освобождение никогда не произойдёт, потому что компилятор не знает когда указатель станет не нужен.
Из очевидных вариантов - использовать GCHandle
самостоятельно:
## uses System.Runtime.InteropServices;
var a := |1,2,3|;
var gc_hnd := GCHandle.Alloc(a, GCHandleType.Pinned);
try
var ptr: ^integer := gc_hnd.AddrOfPinnedObject.ToPointer;
Writeln(ptr);
Writeln(ptr^);
finally
// Освобождение в finally, чтобы оно произошло не зависимо ошибок при выполнении
gc_hnd.Free;
end;
Но на него наложенны некоторые ограничения, связанные с тем как среда .Net работает с памятью:
uses System;
type
r1 = record
b1: byte;
i: integer;
b2: byte;
// s: string[5];
end;
function Offset(p1,p2: pointer) := int64(p2)-int64(p1);
begin
var a: r1;
Offset(@a,@a.b1).Println;
Offset(@a,@a.i ).Println;
Offset(@a,@a.b2).Println;
// Offset(@a,@a.s).Println;
end.
Пока запись содержит только размерные поля - их хранит в памяти в том порядке, которые описали вы.
Ну и в данном случае если бы i
хранилось сразу за b1
- его адрес не делился бы поровну на sizeof(integer)=4
,
а значит процессору было бы неудобно читать и записывать i
, поэтому его отодвигает на 3 байта от b1
.
Но если раскомментировать объявление поля s
, являющегося строкой (то есть имеющего ссылочный тип),
среда .Net решает что эта запись не может быть передана в неуправляемый код как есть,
(потому что каждая реализация .Net может по-своему решать как хранить содержимое классов)
а значит можно спокойно перетасовать поля так, чтобы плотнее их упаковать.
Эта запись всё ещё может быть передана параметром в неуправляемый код, даже var
-параметром.
Но в таком случае данные из неё копирует в отдельную область памяти, так чтобы в этой области
поля были в том порядке, в котором их описали в записи. Это копирование можно симулировать вручную:
uses System;
uses System.Runtime.InteropServices;
type
// Чтобы класс был форматированным, то есть совместимым
// с неуправляемым кодом, надо или
// LayoutKind.Sequential или LayoutKind.Explicit
[StructLayout(LayoutKind.Sequential)]
MyFormattedClass = class
b1: byte := $BA;
i: integer := $87654321;
b2: byte := $DC;
end;
// LayoutKind.Sequential влияет на StructureToPtr,
// но не на то как хранятся данные в переменной "a"
// CharSet.Unicode указывает как должен работать UnmanagedType.ByValTStr
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
r1 = record
b1: byte := $AB;
i: integer := $12345678;
b2: byte := $CD;
o := new MyFormattedClass;
// Явно указываем как неуправляемый код
// должен видеть массив и строку:
// Из массива берём первые 2 значения и сохраняем в саму запись
// В массиве должно быть хотя бы 2 элемента при вызове StructureToPtr
[MarshalAs(UnmanagedType.ByValArray, SizeConst=2)]
arr := new byte[]($EF,$FE);
// А из строки берём максимум 2 первых символа
// То есть не больше двух, в строке может быть и меньше
// В конце всегда добавляется нулевой символ, чтобы
// указать неуправляемому коду где строка заканчивается
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=2+1)]
s := char($CC11)+char($CC22);
end;
function Offset(p1,p2: pointer) := int64(p2)-int64(p1);
begin
var a: r1;
'Содержимое r1:'.Println;
Writeln('Начало: ',@a);
Offset(@a,@a.b1).Println;
Offset(@a,@a.i ).Println;
Offset(@a,@a.b2).Println;
Offset(@a,@a.o).Println;
'Содержимое класса:'.Println;
// Будем надеятся что содержимое класса не переместит пока выводим
// Конечно, в реальной ситуации такая надежда не годится
var po := PPointer(pointer(@a.o))^;
Writeln('Начало: ',po);
Offset(po,@a.o.b1).Println;
Offset(po,@a.o.i ).Println;
Offset(po,@a.o.b2).Println;
var sz := Marshal.SizeOf&<r1>;
var ptr := Marshal.AllocHGlobal(sz);
// Делаем неуправляемую версию этой записи
Marshal.StructureToPtr(a, ptr, false);
'Содержимое неуправляемой копии:'.Println;
for var i := 1 to sz do
begin
// X2 указывает 16-ричный вывод, по 2 символа на каждое значение
PByte((ptr+(i-1)).ToPointer)^.ToString('X2').Print;
if i.Divs(8) then Println;
end;
end.
Поля записи r1
опять перемешиваются, даже с явным указанием LayoutKind.Sequential
,
потому что эта запись всё равно не может использоваться неуправляемым кодом как-есть.
В то же время поля класса оказались в том порядке, как мы указали. Потому что, на самом деле,
этот класс можно передавать в неуправляемый код отдельно от r1
. Такое объявление:
procedure p1(o: MyFormattedClass); external 'some_lib.dll';
Будет эквивалентно:
procedure p1(var o: SomeRecord); external 'some_lib.dll';
Где SomeRecord
- запись с таким же полями. Но есть одно но: первое объявление p1
будет блокировать
содержимое MyFormattedClass
с помощью GCHandle
, тратя на это немного времени процессора.
Но если бы в MyFormattedClass
были ссылочные поля - все его поля перемешались бы так же, как и поля r1
.
Далее, вызывается StructureToPtr
, копирующее содержимое записи в область неуправляемой памяти.
И в данном случае, то что сохраняется в неуправляемую память это не копия содержимого переменной a
,
а совершенно новое значение, в которое влили копии содержимого MyFormattedClass
, массива и строки,
не смотря на то что оригиналы этих значений хранились в разных частях оперативной памяти.
Кроме того, в данном примере не показано, но есть некоторые типы записей, как boolean
, char
и DateTime
,
которые можно по-разному интерпретировать при передаче в неуправляемый код.
Их тоже преобразовывает при копировании в неуправляемую память.
Возвращаясь к GCHandle
: Он принципиально используется не для копирования, а для закрепления в памяти существующего содержимого.
Поэтому когда он встречает запись, которую надо преобразовывать при передаче в неуправляемый код,
GCHandle
наотрез отказывается загреплять её с GCHandleType.Pinned
.
(но в качестве исключения разрешает закреплять некоторые особые случаи, как массив символов)
И это хорошо - это защита от дурака, потому что случаи когда запись, поля которой среда .Net могла перетасовать, передают в неуправляемый код как есть - заведомо неправильны.
Но с другой стороны - эта проверка типов происходит во время выполнения программы.
А если надо делать много быстрых неуправляемых вызовов - это плохо.
Поэтому н.у модули используют другой способ:
Как видно в тестах на странице выше - массив можно заблокировать в памяти без GCHandle
,
если передавать его параметром-массивом или var
-параметром.
И, в отличии от GCHandle
, такой способ будет работать с массивами с любым размерным типом элементов.
Это значит что следить за адекватностью содержимого записи должны вы сами.
Но в то же время это значит что вы сами решаете, какие проверки делать.
К примеру, если имеем процедуру p1
из неуправляемой .dll, принимающую массив из двух чисел типа integer
:
var a := new integer[5](...);
p1(a); // Передача массива целиком
p1(a[3]); // Передача [3] элемента var-параметром
Из первого вызова p1
возьмёт только элементы a[0]
и a[1]
,
потому что p1
по условию требует только два элемента.
Из второго вызова p1
возьмёт a[3]
и a[4]
, потому что в неуправляемом коде
нет разницы между указателем на один из элементов и указателем на начало массива.
Но, обратите внимание, на случай, когда тип требует особого преобразования.
Для примера возьмём параметр, который объявили как var b: boolean
и передали в него элемент массива:
boolean
занимает 1 байт в .Net, но по-умолчанию преобразовывается в BOOL
из C++, занимающий 4 байта.
Поэтому, в данном случае, среда .Net выделит новую неуправляемую область памяти в 4 байта
и передаст её неуправляемому коду. А остальные элементы массива неуправляемому коду не достанутся.
Обычнно эти два способа передать массив в неуправляемый код - всё что вам понадобится.
Но, допустим, вы хотите написать подпрограмму для создания OpenGL буфера из массива векторов. Можно сделать перегрузку для каждого типа вектора, но тогда получится очень много дублей кода. Этого довольно просто избежать, используя шаблоны:
// Это не настоящая подпрограмма, а только пример
procedure FillBuffer(var data: byte);
external 'some.dll';
// external подпрограммы не могут быть шаблонными, поэтому нужна ещё одна промежуточная перегрузка
// "where T: record;" делает так, что FillBuffer будет можно вызвать только для размерных типов T
procedure FillBuffer<T>(var data: T); where T: record;
begin
// Компилятор развернёт это в "FillBuffer(data)"
// То есть никакие преобразования в .exe не попадут
// Но указатели всё равно нужны, чтобы компилятор не ругался на несовместимость типов
FillBuffer(PByte(pointer(@data))^);
end;
procedure FillBuffer<T>(data: array of T); where T: record;
begin
// В неуправляемом коде нет разницы между массивом и адресом начала его содержимого
// Поэтому можно передавать массив в виде [0] элемента-var-параметра.
FillBuffer(data[0]);
end;
Но это для одномерных массивов. А что насчёт многомерных?
Сделать перегрузку для заданного кол-ва измерений не сложно:
procedure FillBuffer<T>(data: array[,] of T); where T: record;
begin
// Многомерные массивы расположены в памяти как одномерные,
// Но обращение к элементам идёт по нескольким индексам
// Элемент [0,0,...] в любом случае будет в самом начале,
// Поэтому код одинаковый для любого кол-ва измерений
FillBuffer(data[0,0]);
end;
Но, опять же, получается так, что для каждой размерности - приходится добавлять перегрузку.
И, к сожалению, в данном случае я не знаю красивого способа обхода.
Лучшее что я могу придумать - создать Dictionary<integer, Action<System.Array>>
,
где ключи - размерности массивов, а значения - делегаты, работающие с соответствующей размерностью.
Когда происходит вызов с массивом определённой размерности - создавать новый делегат
в виде динамичного метода, с помощью System.Reflection.Emit
, если его ещё нет в словаре.
Как и массивы - неуправляемые строки это указатель на первый символ строки.
Но со строками ещё сложнее - исходные библиотеки хранят строки в кодировке ANSI (1 байт на символ).
А управляемые строки - хранят символы в кодировке Unicode (2 байта на символ).
Кроме того, у неуправляемых строк принятно добавлять в конце строки символ #0
.
Это позволяет вообще не хранить длину строки. Вместо этого конец строки считается там, где первый раз встретится символ #0
.
Благо, для перевода между этими типами уже есть Marshal.StringToHGlobalAnsi
и Marshal.PtrToStringAnsi
.
Но будьте осторожны - Marshal.StringToHGlobalAnsi
выделяет неуправляемую память для хранения неуправляемого варианта строки.
Когда неуправляемая память стала не нужна - её надо обязательно удалить методом Marshal.FreeHGlobal
, иначе получите утечку памяти.
В отличии от массивов - пытаться передавать строки в виде символа-var
-параметра безсмысленно, из за разницы форматов содержимого.
Более того, передача символа строки var
-параметром, в отличии от массивов, всё равно вызывает копирование строки,
на всякий случай, потому что в .Net строки неизменяемы, а компилятор не может знать,
будет неуправляемый код только читать, или ещё и перезаписывать строку.
Единственный способ не выполнять лишних копирований - написать свою обёртку неуправляемых строк. Обычно оно того не стоит.
Но если вы, к примеру, создаёте много OpenGL шейдеров из исходников - можно перед компиляцией программы:
Marshal.StringToHGlobalAnsi
чтобы получить неуправляемые строки;$resource
, читать как массив байт и его уже передавать неуправляемому коду вместо строки.Делегат - это адрес подпрограммы:
procedure p1(i: integer);
begin
Writeln(i);
end;
begin
var d: System.Delegate := p1; // Это не вызов, а получение адреса p1
d.DynamicInvoke(5); // Вообще .DynamicInvoke это очень медленно
var p: integer->();
// Такое же объявление как на предыдущей строчке, но в другом стиле
// var p: Action<integer>;
// И ещё один стиль. Этот особенный, потому что
// Он неявно создаёт новый тип делегата
// var p: procedure(i: integer);
p := p1;
// Типизированные делегаты можно вызывать быстрее и проще,
// так же как обычные подпрограммы
p(5);
end.
Так же как обычные подпрограммы - подпрограммы из неуправляемых .dll могут принимать делегаты параметром.
Далее всё будет рассматриваться на примере cl.SetEventCallback
из модуля OpenCL
, потому что с ним есть особые проблемы.
Объявление cl.SetEventCallback
:
static function SetEventCallback(&event: cl_event; command_exec_callback_type: CommandExecutionStatus; pfn_notify: EventCallback; user_data: IntPtr): ErrorCode;
Объявление EventCallback
:
EventCallback = procedure(&event: cl_event; event_command_status: CommandExecutionStatus; user_data: IntPtr);
Рассмотрим следующий пример:
uses System;
uses OpenCL;
begin
var cb: EventCallback := (ev,st,data)->
begin
Writeln($'Активация обработчика ивента {ev}, зарегестрированного для состояния {st}');
end;
var ev: cl_event; //ToDo := ...
cl.SetEventCallback(ev, CommandExecutionStatus.COMPLETE, cb, IntPtr.Zero).RaiseIfError;
end.
Этот код может время от времени вылетать, потому что:
cl.SetEventCallback
вызывает свой коллбек тогда, когда посчитает нужным (но обычно после того как вызов cl.SetEventCallback
завершился);
Делегаты - это классы. А сборщик мусора распоряжается памятью классов и удаляет их, тоже когда посчитает нужным.
Раз после вызова cl.SetEventCallback
делегат cb
больше нигде не используется - сборщик мусора может в любой момент
решить удалить его. Но, опять же, это редко случается сразу после вызова cl.SetEventCallback
,
поэтому ошибки связанные с этим удалением могут быть плавающие.
Если сборщик мусора удалит делегат, а затем .dll попытается его вызвать - это приведёт или к ошибке доступа, или к моментальному беззвучному вылету.
Чтобы запретить сборщику мусора удалять делегать - нужно создать GCHandle
, привязанный к нему.
Но в отличии от массивов - GCHandleType.Pinned
не нужно, потому что сборщик мусора
не может перемещать адрес исполняемого кода (а он единственное что передаётся в .dll).
Это потому, что этот адрес хранится в виде указателя на неуправляемый код.
uses System.Runtime.InteropServices;
uses System;
uses OpenCL;
begin
var gc_hnd: GCHandle;
var cb: EventCallback := (ev,st,data)->
begin
// В данном случае делегат вызывается одноразово
// И пока он выполняется - сборщик мусора его не удалит
// Поэтому освобождение GCHandle можно поставить в любой части самого обработчика
gc_hnd.Free;
Writeln($'Активация обработчика ивента {ev}, зарегестрированного для состояния {st}');
end;
gc_hnd := GCHandle.Alloc(cb);
var ev: cl_event; //ToDo := ...
cl.SetEventCallback(ev, CommandExecutionStatus.COMPLETE, cb, IntPtr.Zero).RaiseIfError;
end.