я столкнулся с проблемой что в моей dll библиотеке...

تم الإنشاء في: ٣ سبتمبر ٢٠٢٥

تمت الإجابة باستخدام GPT-5 Thinking بواسطة Chat01

سؤال

я столкнулся с проблемой что в моей dll библиотеке есть экспортируемая функция и она в свою очередь использует функцию которая использует ещё одну функцию и каждая из этих функций как принимает так и передаёт аргументы - проблема в том что при таком уровне вложенности я получаю SEH исключение в вызывающей стороне, вызывающая сторона - это управляемый код и дело 100 процентов именно в этом, я пробовал передавать аргументы по ссылкам но результат тот же, ещё раз повторюсь что 100 процентов дело именно в этом так как все остальные участки кода я идеально отладил и протестировал в консольном приложении и там всё работает идеально - проблема возникает именно когда делаю функцию экспортитруемой. И управляемый код и нативный код dll библиотеки на c++/c# используют соглашение о вызовах __cdecl

الفكرة

Thought for 1m 11s

الإجابة

Похоже, это типичный P/Invoke-казус: стек/ABI или маршалинг где-то не совпадают, а проявляется это «на выходе» из экспортируемой функции как SEHException в управляемой стороне. Глубина вложенности вызовов тут, почти наверняка, ни при чём — причина обычно в подписи (calling convention, размеры типов, упаковка структур, строки/буферы, исключения и т. п.).

Ниже — короткий чек-лист, который практически всегда находит такую ошибку.

Что проверить в первую очередь

  1. Calling convention совпадает на 100%
  • Экспорт в C/C++: extern "C" __declspec(dllexport) int __cdecl Foo(...);
  • Импорт в C#:
    csharp
    [DllImport("MyLib", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] static extern int Foo(...);

В C# по умолчанию CallingConvention.Winapi → на Windows это чаще всего StdCall, поэтому обязательно указывайте Cdecl. Любое расхождение даёт «перекошенный» стек и SEH.

  1. Размеры и типы параметров/результатов Частые ловушки:
  • size_t/ptrdiff_tна x64 это 8 байт. В C# используйте UIntPtr/IntPtr или nuint/nint, а не int.
  • long в MSVC — 4 байта и на x86, и на x64. В C# под это нужен int, не long.
  • Указатели/дескрипторы → IntPtr/UIntPtr.
  • bool в C++ — 1 байт, а в P/Invoke по умолчанию — WinAPI BOOL (4 байта).
    Если реально bool (C++), пишите:
    csharp
    [MarshalAs(UnmanagedType.I1)] bool
    Если это WinAPI-стиль BOOL, используйте int.
  1. Строки
  • const char* ↔ C#: string, CharSet.Ansi или [MarshalAs(UnmanagedType.LPStr)].
  • const wchar_t* ↔ C#: string, CharSet.Unicode или [MarshalAs(UnmanagedType.LPWStr)].
  • Если у вас UTF-8 в нативном коде, в C# используйте [MarshalAs(UnmanagedType.LPUTF8Str)] (доступно в .NET Core/5+), либо передавайте буфер byte[]/IntPtr.
  • Кто владеет памятью? Нельзя освобождать в .NET то, что выделили new/malloc в C++; либо возвращайте в управляемый код копию, либо предоставьте функцию-освободитель из той же DLL.
  1. Структуры и упаковка
  • С обеих сторон одинаковый #pragma pack/align:
    c
    undefined

// C++ #pragma pack(push, 1) struct MyArgs { bool f; int x; /* ... */ }; #pragma pack(pop)

text
```csharp // C# [StructLayout(LayoutKind.Sequential, Pack = 1)] struct MyArgs { [MarshalAs(UnmanagedType.I1)] public bool f; public int x; }
  • Встроенные массивы: [MarshalAs(UnmanagedType.ByValArray, SizeConst = N)].
  • Не возвращайте из DLL «сложные» структуры по значению — это может менять ABI (скрытый sret-параметр). Лучше int Foo(in MyArgs args, out MyResult result).
  1. Исключения через границу Любое невыловленное C++-исключение превращается в SEH на стороне .NET. Оборачивайте экспорт try/catch(...) и возвращайте код ошибки:
c
extern "C" __declspec(dllexport) int __cdecl DoWork(const MyArgs* inArgs, MyResult* outRes) noexcept { try { return Impl(*inArgs, *outRes); // 0 = OK } catch (...) { return -1; // ошибка } }

И в C# проверяйте код возврата.

  1. Callback’и (если есть)
  • Делегату укажите ту же конвенцию:
    csharp
    [UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate void ProgressCb(int p);
  • Не давайте делегату быть собранным GC: держите GCHandle или статическое поле.
  1. Соберите всё в x64/x86 согласованно
  • Проект C# не «Any CPU» с Prefer 32-bit, когда DLL — x64 (или наоборот). Делайте обе стороны x64 (или обе x86).

Минимальный «эталон» сигнатуры

C++ (MSVC)

cpp
#include <cstddef> #pragma pack(push, 1) struct MyArgs { bool flag; // 1 byte int count; // 4 bytes }; struct MyResult { int code; std::size_t bytes; // 8 bytes on x64 }; #pragma pack(pop) extern "C" __declspec(dllexport) int __cdecl DoWork(const MyArgs* inArgs, MyResult* outRes) noexcept { try { // ... реальная логика, вложенные вызовы и т.д. outRes->code = 0; outRes->bytes = 123; return 0; } catch (...) { return -1; } }

C#

csharp
using System; using System.Runtime.InteropServices; [StructLayout(LayoutKind.Sequential, Pack = 1)] struct MyArgs { [MarshalAs(UnmanagedType.I1)] public bool flag; // C++ bool (1 byte) public int count; } [StructLayout(LayoutKind.Sequential, Pack = 1)] struct MyResult { public int code; public UIntPtr bytes; // size_t } static class Native { [DllImport("MyLib", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true, CharSet = CharSet.Ansi)] // или Unicode, если wchar_t* public static extern int DoWork(in MyArgs args, out MyResult res); }

Диагностика: как быстро найти место перекоса

  1. Включите «Enable native code debugging» у управляемого проекта. Поставьте брейкпоинт на экспорт. Посмотрите call stack.
  2. В Exception Settings включите «Break on thrown» для SEH — упадёте на месте первопричины.
  3. В .NET Framework можно включить MDA PInvokeStackImbalance; в .NET 6/7 ловите признаки «Stack imbalance» через отладчик.
  4. Проверьте экспорт имён:
    dumpbin /exports MyLib.dll
    
    Имя должно быть ровно DoWork (если extern "C" и ExactSpelling=true).
  5. Компилируйте нативный код с защитами:
    • Debug: /RTC1 /GS /sdl
    • x64: убедитесь, что нет варьирующих #pragma pack в разных инклудах.
  6. Если есть строки/буферы — временно замените их на «тупые» значения фиксированной длины и посмотрите, исчезнет ли SEH: это быстро показывает, что виноват маршалинг строки/массива.

Когда имеет смысл поменять подход

Если сигнатуры сложные (вложенные структуры, массивы структур, владение памятью), сделайте тонкий C++/CLI-шлюз:

  • Проект C++/CLI (mixed-mode) с «managed» обёрткой, которая вызывает нативные функции напрямую.
  • C# зовёт только C++/CLI (управляемый) слой — там всё типобезопасно, маршалинг под контролем.

Если хотите, можете скинуть точные подписи экспортируемой функции (C++) и DllImport (C#) + определения структур — я сопоставлю поля/атрибуты и укажу, где именно возникает несоответствие. Но даже без этого этот чек-лист обычно за 1–2 правки убирает ваш SEH.

مشاركة هذا السؤال والجواب