Проблема с интеропом
От: borhes  
Дата: 19.07.10 08:36
Оценка:
Добрый день, коллеги.

Есть сишное апи, такого вида:

typedef void (__stdcall *ReadDataCallback)(void* buf);
int Init(ReadDataCallback pFunction);



По вызову Init запускается некий цикл, переодически вызывающий колбек, и отдающий в него
указатель на массив постоянного и известного мне размера.

Мне надо использовать это апи из С#, для чего я описал интероп следующий образом:

public delegate void ReadDataCallback([MarshalAs(UnmanagedType.LPArray, SizeConst=1024)] byte[] buf);

[DllImport("SomeDll.dll", EntryPoint = "Init")]
public static extern int GeoInit(ReadDataCallback callback);



Данный способ не работает вполне хорошо — в рабочем проекте быстро вываливается исключение
NullReferenceException в безымянном потоке, стек которого отладчик показать не может.
В тестовой консольной программке все хорошо.

Подозреваю, что я неправильно описал интероп.

Идеи?
Re: Проблема с интеропом
От: TK Лес кывт.рф
Дата: 19.07.10 08:39
Оценка: 10 (2)
Здравствуйте, borhes, Вы писали:

B>Идеи?


Частая проблема в том, что экземпляр ReadDataCallback не защищается от сборщика мусора. В итоге указатель, переданный в native код, начинает указывать на уже собранный объект
Если у Вас нет паранойи, то это еще не значит, что они за Вами не следят.
Re: Проблема с интеропом
От: 0x7be СССР  
Дата: 19.07.10 08:41
Оценка: 2 (1)
Здравствуйте, borhes, Вы писали:

B>Идеи?

У меня был такой опыт с callback`ами: я создавал делегат, передавал его в unmanaged и "забывал" про него. Через некоторое время его "собирал" сборщик мусора, поскольку ссылка на него, сохраненная внутри unmanaged-код, GC не учитывается. Проявлялось в том, что спустя некоторое время после нормальной работы вылетало исключение (правда, не помню точно, было ли это NRE).

Вылечил ситуацию тем, что держал искусственную ссылку в managed коде на тот делегат.
Re: Проблема с интеропом
От: _FRED_ Черногория
Дата: 19.07.10 09:18
Оценка:
Здравствуйте, borhes, Вы писали:

B>Есть сишное апи, такого вида:

B>typedef void (__stdcall *ReadDataCallback)(void* buf);
B>int Init(ReadDataCallback pFunction);

B>По вызову Init запускается некий цикл, переодически вызывающий колбек, и отдающий в него
B> указатель на массив постоянного и известного мне размера.
B>Мне надо использовать это апи из С#, для чего я описал интероп следующий образом:
B>public delegate void ReadDataCallback([MarshalAs(UnmanagedType.LPArray, SizeConst=1024)] byte[] buf);

B>[DllImport("SomeDll.dll", EntryPoint = "Init")]
B>public static extern int GeoInit(ReadDataCallback callback);

B>Данный способ не работает вполне хорошо — в рабочем проекте быстро вываливается исключение
B>NullReferenceException в безымянном потоке, стек которого отладчик показать не может.
B>В тестовой консольной программке все хорошо.
B>Подозреваю, что я неправильно описал интероп.
B>Идеи?

Особенно здорово становится, когда рефакторинг этого вот
GeoInit(() => { /* some staff */ });

в
// Вынесли лямбду\анонимный метод в отдельный метод
GeoInit(MyReadDataCallbackMethod);

начинает приводить к проблемам
Help will always be given at Hogwarts to those who ask for it.
Re[2]: Проблема с интеропом
От: Кэр  
Дата: 19.07.10 19:10
Оценка:
Здравствуйте, TK, Вы писали:

B>>Идеи?


TK>Частая проблема в том, что экземпляр ReadDataCallback не защищается от сборщика мусора. В итоге указатель, переданный в native код, начинает указывать на уже собранный объект


Если проблема изначально была в этом, то обратите внимание также на fixed keyword:
http://msdn.microsoft.com/en-us/library/f58wzh21(VS.80).aspx

Цитата из той же страницы:

The marshaler will automatically pin blittable reference types, such as an array, being passed to native code, for the duration of the call. It cannot do this with raw pointers because it doesn't know how much memory they point to. This is also not sufficient if the API retains the pointer, for example to call back asynchronously. In that case you must pin explicitly.

Re[3]: Проблема с интеропом
От: Jolly Roger  
Дата: 20.07.10 02:58
Оценка:
Здравствуйте, Кэр, Вы писали:

Кэр>Если проблема изначально была в этом, то обратите внимание также на fixed keyword:

Кэр>http://msdn.microsoft.com/en-us/library/f58wzh21(VS.80).aspx

Для интеропа fixed, по-моему, практически бесполезен. На время вызова нативного метода маршалер сам "пришивает" ссылочные типы, вместо адреса практически всегда можно обойтись ref или out, а в случаях, когда такие типы должны оставаться доступны нативному коду и после выхода из функции, fixed не поможет, и нужно фиксировать вручную.
"Нормальные герои всегда идут в обход!"
Re[4]: Проблема с интеропом
От: Кэр  
Дата: 20.07.10 04:18
Оценка:
Здравствуйте, Jolly Roger, Вы писали:

JR>Для интеропа fixed, по-моему, практически бесполезен. На время вызова нативного метода маршалер сам "пришивает" ссылочные типы, вместо адреса практически всегда можно обойтись ref или out, а в случаях, когда такие типы должны оставаться доступны нативному коду и после выхода из функции, fixed не поможет, и нужно фиксировать вручную.


Вы правы, спасибо, что заставили меня вернуться. Я просто быстро набросал ответ, пока код на работе собирался и скорее всего не вернулся.

Как я уже сказал, нужно "обратить внимание на". Потом понять, насколько там неуютно шпилить ссылку на делегат с помощью fixed и найти вот этот метод GCHandle.Alloc:
http://msdn.microsoft.com/en-us/library/1246yz8f.aspx

С помощью него удобнее шпилить делегат на неопределенное количество времени. Ну и иметь ввиду, что юзать это надо осторожно, чтобы не получить дефрагментированную кучу.

Ну а вообще лично я считаю интероп в C# довольно неуклюжим — очень много поведения скрыто, все работает как-то "вот так" за кулисами. Работает нормально для простых сишных методов, но у меня всегда руки чешутся в таких случаях взять полноценный С++/CLI и сделать все руками. Код получается в разы более прозрачный. Возможно, что это на мой непросвященный взгляд
Re[5]: Проблема с интеропом
От: borhes  
Дата: 20.07.10 09:53
Оценка:
Здравствуйте, Кэр, Вы писали:

Кэр>Как я уже сказал, нужно "обратить внимание на". Потом понять, насколько там неуютно шпилить ссылку на делегат с помощью fixed и найти вот этот метод GCHandle.Alloc:

Кэр>http://msdn.microsoft.com/en-us/library/1246yz8f.aspx

Забавно то, что в мсдн приводится пример, эквивалентный моему, где они ничего не пинят, а просто сохраняют ссылку на делегат.
http://msdn.microsoft.com/en-us/library/7esfatk4.aspx
Re[6]: Проблема с интеропом
От: borhes  
Дата: 20.07.10 10:06
Оценка:
Даже не на делегат, а на экземпляр класса с методом, связываемым с делегатом. Почему это должно работать?
Re[7]: Проблема с интеропом
От: Jolly Roger  
Дата: 20.07.10 10:34
Оценка:
Здравствуйте, borhes, Вы писали:

B>Даже не на делегат, а на экземпляр класса с методом, связываемым с делегатом. Почему это должно работать?


Видимо потому, что в их примере не требуется, чтобы время жизни делегата было больше чем время работы нативного метода, а на это время маршалер сам пришпилит делегат.
"Нормальные герои всегда идут в обход!"
Re[8]: Проблема с интеропом
От: borhes  
Дата: 20.07.10 10:49
Оценка:
Здравствуйте, Jolly Roger, Вы писали:

JR>Видимо потому, что в их примере не требуется, чтобы время жизни делегата было больше чем время работы нативного метода, а на это время маршалер сам пришпилит делегат.


Так они как раз на эту тему пример и приводят.
[msdn]
To compensate for unexpected garbage collection, the caller must ensure that the cb object is kept alive as long as the unmanaged function pointer is in use. Optionally, you can have the unmanaged code notify the managed code when the function pointer is no longer needed, as the following example shows.
[/msdn]


internal class DelegateTest {
   CallBackClass cb;
   // Called before ever using the callback function.
   public static void SetChangeHandler() {
      cb = new CallBackClass();
      ExternalAPI.SetChangeHandler(new ChangeDelegate(cb.OnChange));
   }
   // Called after using the callback function for the last time.
   public static void RemoveChangeHandler() {
      // The cb object can be collected now. The unmanaged code is 
      // finished with the callback function.
      cb = null;
   }
}
Re[9]: Проблема с интеропом
От: borhes  
Дата: 20.07.10 11:02
Оценка:
Ну и собсно вызов GCHandle.Alloc дает System.ArgumentException: Object contains non-primitive or non-blittable data.
at System.Runtime.InteropServices.GCHandle.InternalAlloc(Object value, GCHandleType type)
at System.Runtime.InteropServices.GCHandle.Alloc(Object value, GCHandleType type)
Re[10]: Проблема с интеропом
От: borhes  
Дата: 20.07.10 11:05
Оценка: 2 (1)
Ну и вот что я нашел:

http://blogs.msdn.com/b/cbrumme/archive/2003/05/06/51385.aspx

"Along the same lines, managed Delegates can be marshaled to unmanaged code,
where they are exposed as unmanaged function pointers. Calls on those
pointers will perform an unmanaged to managed transition; a change in
calling convention; entry into the correct AppDomain; and any necessary
argument marshaling. Clearly the unmanaged function pointer must refer to a
fixed address. It would be a disaster if the GC were relocating that! This
leads many applications to create a pinning handle for the delegate. This
is completely unnecessary. The unmanaged function pointer actually refers
to a native code stub that we dynamically generate to perform the transition
& marshaling. This stub exists in fixed memory outside of the GC heap.

However, the application is responsible for somehow extending the lifetime
of the delegate until no more calls will occur from unmanaged code. The
lifetime of the native code stub is directly related to the lifetime of the
delegate. Once the delegate is collected, subsequent calls via the
unmanaged function pointer will crash or otherwise corrupt the process. In
our recent release, we added a Customer Debug Probe which allows you to
cleanly detect this all too common bug in your code. If you havent
started using Customer Debug Probes during development, please take a look!"
Re[9]: Проблема с интеропом
От: Jolly Roger  
Дата: 20.07.10 12:00
Оценка:
Здравствуйте, borhes, Вы писали:

А я не ходил по ссылке

B>
B>internal class DelegateTest {
B>   CallBackClass cb;
B>   // Called before ever using the callback function.
B>   public static void SetChangeHandler() {
B>      cb = new CallBackClass();
B>      ExternalAPI.SetChangeHandler(new ChangeDelegate(cb.OnChange));
B>   }
B>   // Called after using the callback function for the last time.
B>   public static void RemoveChangeHandler() {
B>      // The cb object can be collected now. The unmanaged code is 
B>      // finished with the callback function.
B>      cb = null;
B>   }
B>}
B>


Это из MSDN? Ну так это неправильный код. Он как раз и даёт эффект, описанный в стартовом посте. Чтобы ускорить проявление, достаточно вызвать GC.Collect между двумя вызовами коллбэка, или даже внутри его.

  Специально сейчас проверил вот таким кодом:
public partial class Form1 : Form
{
    delegate void TestCallback(int step);

    [DllImport("TestDelegate.dll")]
    static extern bool StartTest(TestCallback callback);
    [DllImport("TestDelegate.dll")]
    static extern bool StopTest();

    class Some
    {
        private Form1 owner;

        public Some(Form1 Owner)
        {
            owner = Owner;
        }

        private void Notify(int n)
        {
            owner.label1.Text = n.ToString();
        }
        public void SomeFunc(int step)
        {
            owner.Invoke(new TestCallback(Notify), step);
        }
    }

    public Form1()
    {
        InitializeComponent();
    }

    private Some o;

    private void button1_Click(object sender, EventArgs e)
    {
        if (o != null) return;
        o = new Some(this);
        StartTest(new TestCallback(o.SomeFunc));
    }

    private void button2_Click(object sender, EventArgs e)
    {
        StopTest();
        o = null;
    }

    private void button3_Click(object sender, EventArgs e)
    {
        GC.Collect(2);
    }
}

После нажатия на button3 появляется сообщение о попытке вызова удалённого сборщиком делегата.
"Нормальные герои всегда идут в обход!"
Re[10]: Проблема с интеропом
От: Jolly Roger  
Дата: 20.07.10 12:15
Оценка:
А если подправить вот так

private Some o;
private GCHandle H;

private void button1_Click(object sender, EventArgs e)
{
    if (o != null) return;
    o = new Some(this);
    var d = new TestCallback(o.SomeFunc);
    H = GCHandle.Alloc(d);
    StartTest(d);
}

то работает нормально, делегат спокойно переживает сборку.
"Нормальные герои всегда идут в обход!"
Re[11]: Проблема с интеропом
От: borhes  
Дата: 20.07.10 12:19
Оценка:
Здравствуйте, Jolly Roger, Вы писали:

JR>А если подправить вот так


JR>
JR>private Some o;
JR>private GCHandle H;
JR>
JR>private void button1_Click(object sender, EventArgs e)
JR>{
JR>    if (o != null) return;
JR>    o = new Some(this);
JR>    var d = new TestCallback(o.SomeFunc);
JR>    H = GCHandle.Alloc(d);
JR>    StartTest(d);
JR>}
JR>

JR>то работает нормально, делегат спокойно переживает сборку.

А зачем вообще хендл при таком подходе? Вы же не запинили делегат?
Re[12]: Проблема с интеропом
От: borhes  
Дата: 20.07.10 12:26
Оценка:
B>А зачем вообще хендл при таком подходе? Вы же не запинили делегат?

Только разве что из соображений читаемости кода... Раз хендл, значит мы защищаем от сборщика.
Re[12]: Проблема с интеропом
От: Jolly Roger  
Дата: 20.07.10 12:33
Оценка:
Здравствуйте, borhes, Вы писали:

B>А зачем вообще хендл при таком подходе?


Может затем, что с ним работает, а без него — нет?

B>Вы же не запинили делегат?


Вы имеете в виду GCHandleType.Pinned? Так не нужно, получается Достаточно предотвратить сборку делегата, а остальное обеспечит маршалер, он ведь с этим справляется, когда необходимое время жизни делегата не превышает время вызова нативной функции. Во всяком случае, экспиремент показывает, что Normal достаточно.

Сам объект, кстати, не обязательно сохранять в поле, можно использовать локальную переменную, делегат не даст его собрать.
"Нормальные герои всегда идут в обход!"
Re[13]: Проблема с интеропом
От: Jolly Roger  
Дата: 20.07.10 12:35
Оценка:
Здравствуйте, borhes, Вы писали:

B>>А зачем вообще хендл при таком подходе? Вы же не запинили делегат?


B>Только разве что из соображений читаемости кода... Раз хендл, значит мы защищаем от сборщика.


Вы о чём, какая читаемость? Может мы о чём-то разном говорим?
"Нормальные герои всегда идут в обход!"
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.