Pull to refresh

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;
      
  }

С языка сняли

Unsafe.Read в 2008 не было.

Структура с алиасингом - так себе решение.

Делалось так:

	private static unsafe float Int32BitsToDouble(int floatAsInt32)
	{
		return *(float)&floatAsInt32;
	}

В С/С++ ровно такой же каст вообще-то считается UB ;-)

Ага, только той его разновидностью, которая unspecified, но вполне defined для конкретной платформы.

Пока писал либу сериализации очень много использовал

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;      
  }

InlineArray даст защиту от выхода за границы (через исключение). в вашем случае это полный ансейф, сугубо на ваших плечах отвественность что вы правильно посчитали длины и оффсеты у Array

К сожалению, довольно много (в разы больше чем в скажем в мире JVM). Интеропа очень много и все те проавила описанные тут для него так же верны. Но очень часто скучающие программисты начинают что-то микрооптимимзировать через unsafe

Sign up to leave a comment.

Articles