Исключения и P/Invoke
Перевод поста NogginBops "Exceptions and P/Invoke"
Пост не является авторским и/или официальным
Ссылка на оригинал: Exceptions and P/Invoke
Автор оригинала: NogginBops
В предыдущем посте мы обсуждали callback обработки ошибок GLFW, и я упомянул более сложную причину для добавления собственного callback обработки ошибок. И эта причина — то, как P/Invoke работает с исключениями.
P/Invoke используется для вызова нативных функций из C#, что является отличной возможностью. Именно так OpenTK может использовать GLFW или любую другую нативную зависимость. Вот пример того, как это используется в OpenTK для вызова функций GLFW:
1
2
[DllImport("glfw3.dll", CallingConvention = Cdecl)]
public static unsafe extern Window* glfwCreateWindow(int width, int height, byte* title, Monitor* monitor, Window* share);
Здесь glfwCreateWindow — это C-функция, определённая в нативной зависимости glfw3.dll. Она работает следующим образом: JIT-компилятору сначала нужно загрузить glfw3.dll, найти функцию glfwCreateWindow и вызвать её с указанными аргументами. Но какое отношение это имеет к исключениям?
Когда код C# выбрасывает исключение, нам нужно развернуть стековые фреймы и выполнить все блоки finally, которые мы проходим, а также позволить блокам catch перехватить и обработать исключение. На каждой платформе это делается по-разному, но это означает наличие специфичных для C# метаданных о стеке и о том, какие функции в нем имеют блоки catch или finally для выполнения в случае возникновения исключения. JIT-компилятор C# способен на такое, но это становится очень сложным при использовании C# вместе с другими языками программирования. Например, давайте рассмотрим сценарий с callback обработки ошибок GLFW. У нас есть код C#, который вызывает нативную функцию (функцию GLFW), которая затем вызывает функцию C# (callback обработки ошибок), и эта функция C# выбрасывает исключение.
Это сложная ситуация, потому что JIT-компилятор C# ничего не знает о нативных функциях. Скажем, например, что нативная функция, которую мы вызвали, — это функция C++, которая имеет блок try-finally, который должен быть выполнен. Что должен делать JIT-компилятор C# в этом случае? Один из вариантов — просто игнорировать нативный код и перейти к ближайшему обработчику исключений C#. Это исключительно плохая идея, так как при таком подходе мы намеренно подрываем гарантию, которую даёт finally в C++, что означает, что некоторый критический код (например, снятие блокировки или освобождение памяти) никогда не будет вызван, но программа продолжит работать. Это может вызвать множество проблем. Так каково же решение? Решения этой проблемы нет, это проклятая проблема.
Так что же происходит? Зависит от ситуации. На большинстве платформ это неопределённое поведение и, вероятно, зпрограмма завершится без передачи исключения в какие-либо блоки catch на другой стороне неуправляемого кода, и никакие нативные блоки finally не будут выполнены. За одним исключением — Windows.
В Windows есть нечто, называемое Structured Exception Handling (SEH), что позволяет среде выполнения C# фактически знать об исключениях в нативном коде. Это означает, что мы можем корректно обрабатывать и распространять исключения вверх по коду, который также использует SEH. Таким образом, в Windows возможно сгенерировать исключение в callback из нативного кода. По крайней мере, это было возможно… Во время написания этой статьи я получил неутешительные новости о том, что начиная с .NET 9 и более поздних версий среда выполнения по умолчанию больше не участвует в SEH. Все версии NativeAOT также не участвуют в SEH.
Так что же это означает для нашего callback обработки ошибок, который генерирует исключение? Это означает, что вам, вероятно, захочется переопределить его самостоятельно! Настройте его так, чтобы он записывал ошибки в вашу собственную систему логирования, чтобы он завершал работу приложения, устанавливая определенное состояние, чтобы приложение завершило работу позже в основном потоке, или, что более интересно, перехватывайте исключение и перебрасывайте его в основном потоке самостоятельно.
Почему OpenTK не может сделать это за меня? Ну, на самом деле, в некоторой степени он это делает, но об этом мы поговорим в следующем посте, так что до встречи там!