2009-10-14 3 views
2

Présentation

Je tente d'utiliser P/Invoke pour enregistrer un struct de callbacks avec une dll native. Lorsque vous appelez une fonction qui appelle la DLL native invoquer les rappels, une exception AccessViolationException se produit. J'ai construit un "petit" cas de test qui démontre le comportement composé de 2 fichiers, native.cpp qui compile dans native.dll et clr.cs qui compile dans l'exécutable.Pourquoi est-ce marshaling struct des délégués de rappel peuvent causer une AccessViolationException

native.cpp


extern "C" { 

typedef void (*returncb)(int i); 

typedef struct _Callback { 
    int (*cb1)(); 
    int (*cb2)(const char *str); 
    void (*cb3)(returncb cb, int i); 
} Callback; 

static Callback *cbStruct; 

__declspec(dllexport) void set_callback(Callback *cb) { 
    cbStruct = cb; 
    std::cout << "Got callbacks: " << std::endl << 
     "cb1: " << std::hex << cb->cb1 << std::endl << 
     "cb2: " << std::hex << cb->cb2 << std::endl << 
     "cb3: " << std::hex << cb->cb3 << std::endl; 
} 


void return_callback(int i) { 
    std::cout << "[Native] Callback from callback 3 with input: " << i << std::endl; 
} 

__declspec(dllexport) void exec_callbacks() { 
    std::cout << "[Native] Executing callback 1 at " << std::hex << cbStruct->cb1 << std::endl; 
    std::cout << "[Native] Result: " << cbStruct->cb1() << std::endl; 
    std::cout << "[Native] Executing callback 2 at " << std::hex << cbStruct->cb2 << std::endl; 
    std::cout << "[Native] Result: " << cbStruct->cb2("2") << std::endl; 
    std::cout << "[Native] Executing callback 3 with input 3 at " << std::hex << cbStruct->cb3 << std::endl; 
    cbStruct->cb3(return_callback, 3); 
    std::cout << "[Native] Executing callback 3 with input 4 at " << std::hex << cbStruct->cb3 << std::endl; 
    cbStruct->cb3(return_callback, 4); 
} 

} 

clr.cs


using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Text; 
using System.Runtime.InteropServices; 

namespace clr { 
    public delegate void returncb(Int32 i); 

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)] 
    public delegate int cb1(); 
    [UnmanagedFunctionPointer(CallingConvention.Cdecl)] 
    public delegate int cb2(string str); 
    [UnmanagedFunctionPointer(CallingConvention.Cdecl)] 
    public delegate void cb3(returncb cb, Int32 i); 

    [StructLayout(LayoutKind.Sequential)] 
    struct Callback { 
     [MarshalAs(UnmanagedType.FunctionPtr)] 
     public cb1 c_cb1; 
     [MarshalAs(UnmanagedType.FunctionPtr)] 
     public cb2 c_cb2; 
     [MarshalAs(UnmanagedType.FunctionPtr)] 
     public cb3 c_cb3; 
    } 

    class Program { 
     static int cb1Impl() { 
      Console.WriteLine("[Managed] callback 1"); 
      return 1; 
     } 

     static int cb2Impl(string c) { 
      Console.WriteLine("[Managed] callback 2"); 
      return int.Parse(c); 
     } 

     static void cb3Impl(returncb cb, Int32 i) { 
      Console.WriteLine("[Managed] callback 3"); 
      Console.WriteLine("[Managed] Executing callback to native."); 
      cb(i); 
     } 

     [DllImport("native.dll", CallingConvention = CallingConvention.Cdecl)] 
     static extern void set_callback(ref Callback cb); 

     [DllImport("native.dll", CallingConvention = CallingConvention.Cdecl)] 
     static extern void exec_callbacks(); 

     static void Main(string[] args) { 
      Callback cb; 
      cb.c_cb1 = new cb1(cb1Impl); 
      cb.c_cb2 = new cb2(cb2Impl); 
      cb.c_cb3 = new cb3(cb3Impl); 

      Console.WriteLine("Beginning test."); 
      Console.WriteLine("Sending callbacks: "); 
      Console.WriteLine("cb1: " + Marshal.GetFunctionPointerForDelegate(cb.c_cb1)); 
      Console.WriteLine("cb2: " + Marshal.GetFunctionPointerForDelegate(cb.c_cb1)); 
      Console.WriteLine("cb3: " + Marshal.GetFunctionPointerForDelegate(cb.c_cb1)); 
      set_callback(ref cb); 
      exec_callbacks(); 
      Console.ReadLine(); 
     } 
    } 
} 


Résultat

Invoquer il en résulte exec_callbacks() lancer un AccessViolationException. cb1 est invoqué avec succès, mais cb2 ne l'est pas. En outre, le code natif montre qu'avant l'appel de cb2, son adresse a changé. Pourquoi cela se produit-il? Au meilleur de ma connaissance, aucun des délégués n'aurait dû être nommé. Comme information supplémentaire, marshalling une structure de IntPtr et en utilisant Marshal.GetFunctionPtrForDelegate fonctionne correctement (même pour cb3 qui obtient un ptr natif à invoquer), cependant, être capable de rassembler les délégués directement est plus logique/est plus lisible.

Répondre

1

Le problème est que CB1, CB2 et CB3 sont alloués tas, même si leur stockage (struct) n'est pas. Ainsi ils sont chacun soumis à GC (compactage/relocalisation, invalidant ainsi les pointeurs passés à l'origine).

Avant de passer dans la structure, chacun de cb1, cb2 et cb3 doit être épinglé, idéalement immédiatement après qu'ils sont nouveaux. Sinon, ils vont probablement déménager en mémoire.

La décision d'utiliser une structure pour construire une carte de fonction classique a-t-elle été prise pour éviter cette relocalisation? Si c'est le cas, ce n'est finalement pas utile.

0

Tout d'abord, félicitations pour avoir publié un code de repro clair. Maintenant, quelques problèmes avec votre code:

  • Plus important encore, le pointeur cb que vous recevez dans set_callback (du C# paramètre ref) et le stockage dans cbStruct n'est pas sûr à stocker. Il n'y a aucune garantie que la structure vers laquelle il pointe restera après le retour de set_callback. Si vous changez votre code afin qu'une copie de la structure soit passée par valeur à la place, je pense que vos erreurs disparaîtront.

  • Les trois appels Marshal.GetFunctionPointerForDelegate reçoivent le premier délégué.

  • Si vous voulez être sûr que les délégués restent valides, insérez les appels à GC.KeepAlive (cb.c_cb1) etc après l'appel à exec_callbacks.

+0

Si possible, je voudrais ne pas avoir à changer le code natif. Y at-il quelque chose qui se traduira par un pointeur vers struct? – Ryan

+0

Oui, si vous changez la signature de set \ _callback pour prendre un IntPtr au lieu d'un callback ref. Ensuite, vous pouvez utiliser le type GCHandle pour obtenir un pointeur vers une copie encadrée de la structure et passer dans celui-ci. Libérez le GCHandle après l'exécution de exec \ _callbacks. –

+0

Quelque chose ne se traduit pas correctement, maintenant aucun des callbacks ne semble être à l'emplacement correct, c'est-à-dire avec quelques changements cosmétiques, mon code montre que les deux gérés/non gérés sont d'accord avec la structure, mais ne sont pas d'accord sur l'emplacement des callbacks callbacks d'envoi: Struct: 291194 CB1: 340A1A CB2: 340B62 CB3: 340BDA callbacks Got: Struct: 00291194 CB1: 02838F34 CB2: 02838DD0 CB3: 00000000 [Native ] Exécution du rappel 1 à 02838F34 – Ryan

0

J'ai lutté avec un problème similaire il y a quelques jours. Bien que le pointeur sur la structure reste valide, les pointeurs de fonction withhin la structure ont changé après le retour du premier rappel. Mon approche était exactement comme indiqué dans votre exemple de code. Au lieu de passer une copie de la structure par val, j'ai décidé de passer un pointeur et de copier la structure vers laquelle il est pointé dans set_callback, cela fonctionne.

__declspec(dllexport) void __stdcall set_callback(Callback *cb) { 
    cbStruct = new Callback(*cb); 
    // TODO: Clean up memory e.g. in release_callback() 
} 
Questions connexes