2009-10-14 57 views
2

介紹

我試圖使用P/Invoke來註冊回調的結構與本地的dll。當調用一個使原生dll調用回調的函數時發生AccessViolationException。我構建了一個「小」測試用例,演示了由編譯爲可執行文件的native.dll和clr.cs的2個文件native.cpp組成的行爲。爲什麼編組回調代表的結構導致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(); 
     } 
    } 
} 


結果

調用這導致exec_callbacks()拋出AccessViolationException。 cb1被成功調用,但cb2不成功。此外,本地代碼顯示在調用cb2之前,其地址已更改。爲什麼會發生?據我所知,沒有一個代表應該被接受。作爲附加信息,編組IntPtr的結構和使用Marshal.GetFunctionPtrForDelegate可以正常工作(即使對於獲取本地ptr來調用的cb3),但是,能夠直接編組代碼更有意義/更具可讀性。

回答

1

問題是cb1,cb2和cb3是堆分配的,儘管它們的存儲(結構)不是。因此,它們都受到GC(壓縮/重定位,因此使最初傳入的指針無效)。

在傳遞結構之前,cb1,cb2和cb3中的每一個都應該被固定,最好是在new'd之後立即固定。否則,他們可能會重新定位在記憶中。

是否決定使用結構來構造經典函數圖來完成此操作以避免此重定位?如果是這樣,它最終沒有幫助。

0

首先,發佈明確repro代碼的榮譽。現在與您的代碼幾個問題:

  • 最重要的是,CB指針你在set_callback接收(從C#ref參數)和cbStruct存儲是不可靠的存儲。不能保證它指向的結構將在set_callback返回後保留。如果你改變你的代碼,所以結構的副本是通過值傳遞的,我認爲你的錯誤將會消失。

  • 所有三個Marshal.GetFunctionPointerForDelegate調用都傳遞給第一個委託。

  • 如果您想確保委託保持有效,請在調用exec_callbacks之後插入對GC.KeepAlive(cb.c_cb1)等的調用。

+0

如果可能,我想不必更改本機代碼。有沒有什麼會轉化爲指向結構的指針? – Ryan 2009-10-15 14:46:37

+0

是的,如果您更改set \ _callback的簽名以採用IntPtr而不是ref回調。然後,您可以使用GCHandle類型來獲取指向結構的盒裝副本的指針並傳入該指針。在exec \ _callbacks返回後釋放GCHandle。 – 2009-10-16 10:58:47

+0

有些東西沒有正確翻譯,現在沒有一個回調看起來是在正確的位置,即有一些美觀的改變,我的代碼顯示託管/非託管同意結構在內存中的位置,但似乎不同意在回調的位置 發送回調: 結構:291194 CB1:340A1A CB2:340B62 CB3:340BDA 了回調: 結構:00291194 CB1:02838F34 CB2:02838DD0 CB3:00000000 [本地]在02838F34處執行回撥1 – Ryan 2009-10-16 16:06:50

0

幾天前我在類似的問題上掙扎。雖然指向結構體的指針仍然有效,但是在第一次回調返回後,結構體中的函數指針發生了變化。我的方法完全如您的示例代碼所示。我決定傳遞一個指針並複製它在set_callback中指向的結構,而不是通過val傳遞結構的副本。

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