2016-07-07 5 views
1

У меня возникли проблемы с повреждением кучи в приложении .NET, которое использует собственный C-код, C++/CLI и C#. Это мой первый раз, действительно попадая в сорняки здесь.Повреждение кучи при обертке неуправляемых указателей в C++/CLI

Структура приложения - это C# для GUI и общий поток управления, C++/CLI для упаковки собственных C-функций и собственные C-функции для обработки данных. Эти нативные функции C обычно принимают в качестве исходных указателей на массивы (например: int *) и измерение. C++/CLI завершает эти низкоуровневые функции в функции комбинированной обработки более высокого уровня, а C# вызывает функции высокого уровня.

Иногда мне нужно выделить неуправляемую память на уровне C#, а затем передать одну и ту же партию памяти нескольким другим функциям C++/CLI.

Чтобы свободно передавать эти массивы через мои слои C# и C++/CLI, я создал тонкий класс-оболочку вокруг управляемых указателей. Эта оболочка, называемая ContiguousArray, определяется на уровне C++/CLI, выглядит примерно так:

template <typename T> 
public ref class ContiguousArray 
{ 
public: 
    ContiguousArray<T>(int size) 
    { 
    _size = size; 
    p = (T*) calloc(_size,sizeof(T)); 
    } 

    T& operator[](int i) 
    { 
    return p[i]; 
    } 

    int GetLength() 
    { 
    return _size; 
    } 
    ~ContiguousArray<T>() 
    { 
    this->!ContiguousArray<T>(); 
    } 

    !ContiguousArray<T>() 
    { 
    if (p != nullptr) 
    { 
     free(p); 
     p = nullptr; 
    } 
    } 

    T* p; 
    int _size; 
}; 

// Some non-templated variants of ContiguousArray for passing out to other .NET languages 
public ref class ContiguousArrayInt16 : public ContiguousArray<Int16> 
{ 
    ContiguousArrayInt16(int size) : ContiguousArray<Int16>(size) {} 
}; 

Я использую этот класс-обертку несколько способов.

пример 1 (C++/CLI):

{ 
    // Create an array for the low level code 
    ContiguousArray<float> unmanagedArray(1024); 

    // Call some native functions 
    someNativeCFunction(unmanagedArray.p, unmanagedArray.GetLength()); 
    float* unmanagedArrayPointer = unmanagedArray.p; 
    anotherNativeCFunction(unmanagedArrayPointer, unmanagedArray.GetLength()); 
    int returnCode = theLastNativeCFunction(unmanagedArray.p, unmanagedArray.GetLength()); 

    return returnCode; 
} // unmanagedArray goes out of scope, freeing the memory 

Использования 2 (C++/CLI):

{ 
    // Create an array for the low level code 
    ContiguousArray<float>^ unmanagedArray = gcnew ContiguousArray<float>(1024); 
    cliFunction(unmanagedArray); 
    anotherCLIFunction(unmanagedArray); 
    float* unmanagedArrayPointer = unmanagedArray->p; 
    int returnCode = nativeFunction(unmanagedArrayPointer, unmanagedArray->GetLength()); 
    return returnCode; 
} // unmanagedArray goes out of scope, the garbage collector will take care of it at some point 

Применение Случай 3 (С #):

{ 
    ContiguousArrayInt16 unmanagedArray = new UnmanagedArray(1024); 
    cliFunction(unmanagedArray); 
    unmanagedArray = anotherCLIFunctionThatReplacesUnmanagedArray(unmanagedArray); // Unmanaged array is possibly replaced, original gets collected at some point 
    returnCode = finalCLIFunction(unmanagedArray); 
    // Do something with return code like show the user 
} // Memory gets freed at some point 

Я думал, что я очень осторожен с обработкой неуправляемой памяти, используя этот класс-оболочку, но я продолжаю наблюдать за коррупцией кучи и проблемами нарушения доступа в моей заявке. Я никогда не сохраняю собственный указатель на неуправляемую память за пределами области действия, где объект ContiguousArray действителен.

Есть ли что-то неправильное в любом из этих трех вариантов использования, которые теоретически могут вызвать кучевое повреждение? Я что-то пропустил в своей реализации ContiguousArray? Я беспокоюсь, что, возможно, сборщик мусора становится немного переусердствующим и очищает мои управляемые объекты, прежде чем я действительно с ними справится.

Использование случая 1: Я уверен, что финализатор не будет вызываться до закрытия скобки? Возможно ли, что .NET решил, что объект больше не используется, и он очищается, а у меня все еще есть указатель на его внутреннюю память? Должен ли GC :: KeepAlive использоваться для объектов стека?

Вариант использования 2: Нужна ли мне GC :: KeepAlive в конце, чтобы гарантировать, что объект не будет удален до вызова третьей функции? Я бы все еще нуждался в этом, если бы я написал: nativeFunction (unmanagedArray-> p, unmanagedArray-> GetLength());

Случай использования 3: Я не вижу здесь ничего плохого, но, может быть, я чего-то не хватает?

+0

Посмотрите на http://www.pinvoke.net. Дает образцы всех родных библиотек. – jdweng

+0

Функции C, которые я вызываю, не являются встроенными библиотеками Windows, а кодом, который я сам компилирую. Я не хочу использовать pinvoke для вызова собственной функции, упражнение - это самообучение для высокой производительности. –

+2

Это, безусловно, неправильный код, финализатор может работать во время выполнения собственного кода. Случается, когда другой поток в программе запускает сборку мусора. Использование GC :: KeepAlive() - это обходной путь, но у вас есть намного лучший вариант: уничтожить массив детерминированным образом. Добавьте «delete unmanagedArray;» или используйте семантику стека, в коде C# используйте 'using'. –

ответ

1

Благодаря волшебству написания моего вопроса (лучшего учителя) и советам цанди и Ганса, я подробно исследовал поведение сборщика мусора при работе с неуправляемыми ресурсами. Вот что я нашел:

Недостаток шаблона проектирования, который я использую. Если сборщик мусора решает, что дескриптор управляемого объекта (^) больше не используется, , даже если дескриптор все еще находится в области, это может быть сбор мусора. Правильный (но медленный) шаблон проектирования не позволяет получить доступ к неуправляемым ресурсам, кроме как с помощью методов его управляемого класса-оболочки. Если указатели или ссылки на неуправляемые ресурсы могут просачиваться из оболочки, то код, который их удерживает, должен проявлять большую осторожность, чтобы убедиться, что обертка, которая их владеет, не получает сбор/финализацию. По этой причине классы-оболочки, созданные как ContiguousArray, не являются хорошей идеей.

Это говорит, что этот образец быстро! Так вот, как спасти вещи в каждом конкретном случае.

Use Case 1 на самом деле нормально! Использование семантики стека в C++/CLI обеспечивает детерминированное завершение, когда оболочка выходит за рамки. Удержание указателей после того, как оболочка вышла из сферы действия, по-прежнему является ошибкой, но во всем это безопасно. Я сильно изменил свой код C++/CLI, чтобы сильно поддержать семантику стека, в том числе используя ссылки на дескрипторы (%), когда это возможно, в качестве аргументов функций, вызываемых только моим кодом C++/CLI.

Использование Вариант 2 опасен и требует фиксации. Иногда вы не можете избежать использования ручек, поэтому вам нужно использовать GC::KeepAlive(unmanagedArray), чтобы заставить сборщик мусора удерживать объекты до вызова KeepAlive.

{ 
    // Create an array for the low level code 
    ContiguousArray<float>^ unmanagedArray = gcnew ContiguousArray<float>(1024); 
    cliFunction(unmanagedArray); 
    anotherCLIFunction(unmanagedArray); 
    float* unmanagedArrayPointer = unmanagedArray->p; 
    int returnCode = nativeFunction(unmanagedArrayPointer, unmanagedArray->GetLength()); 
    GC::KeepAlive(unmanagedArray); // Force the wrapper to stay alive while native operations finish. 
    return returnCode; 
} 

Использование 3-го варианта технически небезопасно. Сразу после вызова finalCLIFunction сборщик мусора .NET может решить, что ему больше не нужен unmanagedArray (в зависимости от реализации finalCLIFunction). Но не имеет смысла обременять код C# с деталями реализации, такими как KeepAlive, если нам это не нужно. Вместо этого никогда не пытайтесь получить доступ к чему-либо неуправляемому из кода C# и убедитесь, что реализация всех наших функций C++/CLI вызывает KeepAlive для собственных аргументов, если эти аргументы являются дескрипторами.

int finalCLIFunction(ContiguousArrayInt16^ unmanagedArray) 
{ 
    // Do a bunch of work with the unmanaged array 
    Int16* ptr = unmanagedArray->p; 
    for(int i=0; i < unmanagedArray->GetLength(); i++) 
    { 
    ptr[i]++; 
    } 

    // Call KeepAlive on the calling arguments to ensure they stay alive 
    GC::KeepAlive(unmanagedArray); 

    return 0; 
} 

Тогда вот оно. Используйте семантику стека, когда это возможно. Когда вы не можете, используйте GC :: KeepAlive() после последней строки, где вам нужны объекты, которые будут живы. Не забудьте также сделать это для вызова аргументов для функций C++/CLI. Храните все эти мусорные корзины в стороне от вашего кода на C#, который не должен знать эти детали реализации.

Я следил за всеми этими соглашениями, и мое повреждение кучи и нарушения доступа исчезли. Надеюсь, это кому-то поможет.

+0

Неплохой совет, но забудьте один вызов GC :: KeepAlive(), и вы снова напортачили. Лучше всего понять конечный недостаток подхода, вы надеетесь, что GC поможет вам справиться с неуправляемыми выделениями памяти, а также просто не может. Избавьтесь от обертки полностью и вызовите malloc/free, как обычно вам нужно в программе на C, что заставляет вас делать это правильно. –

+0

Я думаю, что это справедливая точка. Но я также думаю, что до тех пор, пока библиотека C++/CLI будет тщательно написана в соответствии с этими соглашениями, вы можете создать неуправляемые ресурсы из кода C#, разрешить сборщику мусора .NET управлять этими ресурсами и вызвать неуправляемые функции для их обработки, без загрязняя ваш код C# с помощью управления памятью или нести дорогостоящие штрафные санкции. Вся сложность сбрасывается в слой C++/CLI, где можно надеяться получить право один раз, а затем забыть. –

+0

Речь идет не о написании кода, а о том, как отлаживать его, когда он терпит неудачу. Исходная ошибка не была отлаживаемой. –

0

Прежде всего, я предполагаю, что член в ContiguousArray<T> называется size вместо _size - это только опечатка.

Что касается нарушений доступа, я не вижу ничего плохого в случае 3. В случае 2 массив может определенно быть собранным мусором до того, как nativeFunction будет выполнен с использованием указателя. Я не уверен, имеет ли случай 1 ту же проблему.Если вы используете GC::KeepAlive, это устраняет нарушения доступа?

Возможно, повреждение кучи означает, что память уже была освобождена к тому времени, когда она была освобождена в !ContiguousArray<T>(). Имеют ли собственные методы когда-либо освобождают массив или делают ContiguousArrays когда-либо своп принадлежащих массивам?

P.S., это хорошая идея, чтобы проверить, что calloc не возвращается nullptr.

+0

Да, извините за _size, я вырезал много вещей из своего кода, чтобы понять суть проблемы, и я не смог правильно реорганизовать. Я отредактирую вопрос. Я занимаюсь редактированием всех примеров, таких как case 2, для использования keepalives, и я отчитаюсь. Нативные функции никогда не пытаются освободить указатели, переданные им (это было бы зло!), И ContiguousArrays никогда не заменяют принадлежащие им массивы, потому что это сделает управление настоящей головной болью. Спасибо за отзыв о распределении. –

Смежные вопросы