Comments 29
Еще докину такой удобный класс как GCHandle который позволяет иметь нативный поинтер на .NET объект. Удобно это тем, что можно "связать" .NET объект с нативной структурой. Например присохранить в OpenSSL контекст ссылку на .NET аналог этого контекста, через BIO_set_data, а потом в колбеках из его восстанавливать:
void InitOpenSslContext(IntPtr bioPtr)
{
OpenSSLContext context = ...;
GCHandle contextHandle = GCHandle.Alloc(context);
IntPtr gcHandlePtr = GCHandle.ToIntPtr(gch);
BIO_set_data(bioPtr, gcHandlePtr);
}
// в колбеках из нативного кода обычно есть только поинтер на нативный контекст
// этой либы, а нам нужен наш .NET объект, по этому мы его достаем его из поля data
// у "зрелых" библиотек обычно есть такое поле и методы доступа к нему
bool WriteEndCallback(IntPtr bioPtr, ...)
{
IntPtr gcHandlePtr = BIO_get_data(bioPtr);
GCHandle contextHandle = GCHandle.FromIntPtr(gcHandlePtr);
OpenSSLContext context = (OpenSSLContext)gch.Target;
// ...
}Еще есть не менее удобный Unsafe.AsRef который позволяет замапить структуру из нативной памяти на .NET стуктуру (не маршалинг), а потом обращаться к ней как к обычной .NET структуре.
[StructLayout(LayoutKind.Sequential, Size = 216)]
internal struct uv_getaddrinfo_t : uv_req_i
{
IntPtr data;
uv_req_type type;
// ... UV_REQ_PRIVATE_FIELDS
IntPtr loop;
}
static unsafe void GetAddressInfoCallback(IntPtr req, int statusCode, IntPtr res)
{
ref var getAddressInfoRequest = ref Unsafe.AsRef<uv_getaddrinfo_t>((void*)req);
// getAddressInfoRequest тут не копия структуры, а типобезопасная ссылка на
// ту же область памяти на которую узказывал `IntPtr req`.
switch (getAddressInfoRequest.type)
{
case uv_req_type.UV_CONNECT: // ...
/// ...
}
}Я лично не сильно фанат заметания unsafe под ковер. если у вас интероп слой, то и используйте в нем uv_getaddrinfo_t* и маршаллить ничего не надо, сразу в сигнатуре параметра объявить
Ну разница между ref uv_getaddrinfo_t и uv_getaddrinfo_t* всё таки есть. На ref вариант можно написать расширение, на поинтер нет:
public static void GetStatus(this ref uv_getaddrinfo_t addrInfo)
{
var addrInfoPtr = (IntPtr)Unsafe.AsPointer(ref handle);
// ...
// native call here
}И это будут типобезопасные методы на ref ссылке, их нельзя будет выполнить на инстансе (ошибиться), только на ссылке.
Ну и второй более замороченный кейс, это наследование структур, можно сделать через интерфейсы и расширения:
// HandleT could be:
// struct uv_async_t : uv_handle_i {}
// struct uv_pipe_t : uv_stream_i, uv_handle_i {}
// struct uv_tcp_t : uv_stream_i, uv_handle_i {}
// etc...
[MustUseReturnValue]
public static bool IsActive<HandleT>(this ref HandleT handle) where HandleT : struct, uv_handle_i
{
var handlePtr = (IntPtr)Unsafe.AsPointer(ref handle);
ThrowIfNull(handlePtr, nameof(handle));
ThrowIfHandleInWrongLoop(handlePtr);
return uv_is_active(handlePtr) > 0;
}Но это сугубо приседания над libuv, где uv_handle_t базовая структура, а от нее все наследуются.
var handlePtr = (IntPtr)Unsafe.AsPointer(ref handle);
Сразу видно, не читали статью ;-) вы тут делаете предположение что handle всегда на стеке или в нативной памяти, хотя ничего не помешает ему быть, например, полем класса или статическим полем и привет GC corruption баг (если это не ref struct конечно).
Т.е. вы используете неявный контракт вызова вашего метода, который никому не известен
Нет, читал, но конечно синьерам запреты не писаны.
И да, вы правы, но C# не даёт сделать `ref struct` ограничение на generic параметр, что бы обезопасить полностью от неправильного использования. Хотя можно делать расширения на `ref struct` типы. К счастью, это всё часть внутреннего кода, и не видна пользователям библиотеки.
btw, те кто дочитали до этого места, проголосуйте пожалуйста за этот PR в репозитории libuv, это очень поможет мне и другим разработчикам в будущем:
github.com/libuv/libuv/pull/4739 -> возможноcть перекидывать сокеты меджу UV loops.
Интересно б статистику конечно, много ли вообще кому не практике приходилось использовать unsafe сам по себе без нужд интеграции с чем-то нативным?
Пример: BitConverter.Int64BitsToDouble был всегда, но BitConverter.Int32BitsToSingle появился сильно позже того, когда он мне был нужен для сборки float-а из последовательности байтов ещё в netfx3.5 в ~2008 году. Пришлось делать через unsafe, но в реализации не было ничего из списка выше.
private static unsafe float UInt32BitsToDouble(uint floatAsInt32)
{
return Unsafe.Read<float>(&floatAsInt32);
}До варианта с Unsafe, был вариант с поинтерами, и даже через C union:
[StructLayout((LayoutKind.Explicit))]
struct Float32ToInt
{
[FieldOffset(0)]
public int IntValue;
[FieldOffset(0)]
public float Float32Value;
}Пока писал либу сериализации очень много использовал
public static void CopyBlockUnaligned(ref byte destination, ref byte source, uint byteCount)
public static void InitBlockUnaligned(ref byte startAddress, byte value, uint byteCount);Даже не знаю, что эти методы делают в Unsafe т.к. границы блоков памяти известны.
Еще много в приватных методах, для перфоманса, но тут надо быть акуратным т.к. в сочетании в [SkipLocalsInit] в вызывающем методе может привести к мусору в out поле:
Unsafe.SkipInit(out value);Я после статьи пошел выяснять, когда можно использовать Unsafe.AsPointer и узнал о Pinned Object Heap
POH не очень удачная фича по итогу получилась, лучше бы ее вообще не делали. Публичные апи к ней привели к тому что смесь долго- и коротко- живущий пиннед массивов приводят в дикой фрагментизации кучи которую нельзя дефрагментировать.
Даже не знаю, что эти методы делают в Unsafe т.к. границы блоков памяти известны.
они являются unsafe потому что ничего вам не мешает ошибится с размером и это не вызовет никаких рантайм проверок. Вот пример:
byte[] a = new byte[10];
byte[] b = new byte[10];
Unsafe.InitBlockUnaligned(ref a[0], value: 42, byteCount: 40);
такой код даже отработает (и станет кандидатом в CVE)
Там был период когда net core только вышел и появился memory.pin, что стало можно всякие перформансные штучки хитрые делать. Векторизация скажем нормальная только через unsafe писалась. Но потом они многие места закрыли более адекватными АПИ высокоуровневыми и после .Net 5 в целом unsafe перестал тут иметь смысл.
А так еще unsafe для fixed size buffer иногда полезен, ну и какие-то грязные low level издевательства над данными, но тоже уже довольно редко требуется.
Как stackalloc стал доступен из safe, так количество unsafe кода у меня резко упало. А со всеми этими ref struct, span и прочими улучшениями стековой аллокации, так все реже и реже требуется.
А так еще unsafe для fixed size buffer иногда полезен
Хотелось бы, чтобы fixed-size buffers полностью ушли в прошлое и заменились inline arrays.
Как stackalloc стал доступен из safe
По текущему плану C# команды, даже если вы используете Span<> x = stackalloc но при этом у вас где-то (над методом или во всей сборке) указан skiplocalsinit, то всё выражение всё равно начнет требовать unsafe context.
Проблема inline arrays видится в том, что это какая-то непонятная компиляторная магия, примотанная сбоку изолентой
Что конкретно непонятно? В целом, можно было реализовать через const generics, но неизвестно будут ли они когда-либо из-за сложности
То, что он не выглядит как привычная языковая конструкция: какое-то поле внутри, какой-то атрибут снаружи. std::array<int, 10> в крестах выглядит куда приятнее, чем вот это всё в C#.
std::array<int, 10> -- это и есть const generics, под них надо изменять метадату, может когда и сделают, но для этого нужна везкая причина (в идеале, еще фичи которым нужны изменения)
Господа, вы тут спорите о синтаксисе fixed array, не могли бы накинуть пример применения fixed array. Мне не приходилось их использовать, даже в interop, ведь можно бахнуть explicit layout и просто пропустить Х байт, типа:
[StructLayout(LayoutKind.Explicit)]
struct MyStruct
{
[FieldOffset(0)]
public int Value1;
[FieldOffset(4)]
public byte ArrayStart;
[FieldOffset(40)]
public int Value2;
}К сожалению, довольно много (в разы больше чем в скажем в мире JVM). Интеропа очень много и все те проавила описанные тут для него так же верны. Но очень часто скучающие программисты начинают что-то микрооптимимзировать через unsafe
Гайд: Как прострелить ноги unsafe кодом в C#