【问题标题】:F# interface with Tcl/Tk results in memory corruptionF# 与 Tcl/Tk 的接口导致内存损坏
【发布时间】:2017-05-08 17:25:31
【问题描述】:

我正在编写一个使用 Tcl/Tk 的 F# 脚本。根据我从 F# 调用 Tcl/Tk 的方式,我遇到了内存损坏。我最好的猜测是内存损坏是因为 F# 垃圾收集器在我分配内存时移动了函数的位置。 Tcl/Tk 显然不知道这个运动,所以坏事发生了。我试图理解为什么一种方法会产生内存损坏而另一种方法不会。

显示问题的最简单代码如下:

open System
open System.Runtime.InteropServices

// -----Tcl-----
// Change this literal to point to appropriate Tcl .dll
[<Literal>]
let TclDll = "c:/Tcl32/bin/tcl86.dll"

// Must have the MarshalAs attribute for objs:ObjPtr[] otherwise the array is assumed to be of size 1.
// The SizeParamIndex parameter specifies the int (the second argument) as being the length of the array.
[<UnmanagedFunctionPointer(CallingConvention.Cdecl)>]
type ObjCmdProc = delegate of nativeint * nativeint * int * [<MarshalAs(UnmanagedType.LPArray, SizeParamIndex=2s)>]objs:nativeint[] -> int

[<UnmanagedFunctionPointer(CallingConvention.Cdecl)>]
type CmdDeleteProc = delegate of nativeint -> unit

// 94 EXTERN Tcl_Interp * Tcl_CreateInterp(void);
[<DllImport(TclDll, EntryPoint="Tcl_CreateInterp", CallingConvention=CallingConvention.Cdecl)>]
extern nativeint tclCreateInterp()

// 180 EXTERN int Tcl_Init(Tcl_Interp *interp);
[<DllImport(TclDll, EntryPoint="Tcl_Init", CallingConvention=CallingConvention.Cdecl)>]
extern int tclInit(nativeint interp)

// 96 EXTERN Tcl_Command Tcl_CreateObjCommand(Tcl_Interp *interp, const char *cmdName, Tcl_ObjCmdProc *proc, ClientData clientData, Tcl_CmdDeleteProc *deleteProc);
[<DllImport(TclDll, EntryPoint="Tcl_CreateObjCommand", CallingConvention=CallingConvention.Cdecl)>]
extern nativeint tclCreateObjCommand(nativeint interp, string cmdName, ObjCmdProc proc, nativeint clientData, CmdDeleteProc deleteProc)

// 291 EXTERN int Tcl_EvalEx(Tcl_Interp *interp, const char *script, int numBytes, int flags);
[<DllImport(TclDll, EntryPoint="Tcl_EvalEx", CallingConvention=CallingConvention.Cdecl)>]
extern int tclEvalEx(nativeint interp, string script, int numBytes, int flags)

// -----Tk-----
// Change this literal to point to appropriate Tk .dll
[<Literal>]
let TkDll = "c:/Tcl32/bin/tk86.dll"

// 0 EXTERN void Tk_MainLoop(void);
[<DllImport(TkDll, EntryPoint="Tk_MainLoop", CallingConvention=CallingConvention.Cdecl)>]
extern void tkMainLoop()

// 118 EXTERN int Tk_Init(Tcl_Interp *interp);
[<DllImport(TkDll, EntryPoint="Tk_Init", CallingConvention=CallingConvention.Cdecl)>]
extern int tkInit(nativeint interp)

// -----Main program-----
// Initialize the Tcl/Tk interpreter
let interp = tclCreateInterp()
tclInit(interp) |> ignore
tkInit(interp) |> ignore

// Direct ObjCmdProc
let testMemDirect = ObjCmdProc (fun clientData interp objCount objs ->
    let mem = seq {for i in 0..1000000 -> i} |> Seq.toList
    printf "%A" mem
    0)

// Indirect ObjCmdProc (via createObjCmd)
let testMemIndirect (objs:nativeint []) (interp:nativeint) = 
    let mem = seq {for i in 0..1000000 -> i} |> Seq.toList
    printf "%A" mem
    0

// Creates an ObjCmdProc given a function
let createObjCmdProc fn = ObjCmdProc (fun clientData interp objCount objs ->
    fn objs interp)

// Ignore delete messages
let ignoreDelete = CmdDeleteProc ignore

// Create Tcl commands
tclCreateObjCommand(interp, "testMemDirect", testMemDirect, IntPtr.Zero, ignoreDelete) |> ignore
tclCreateObjCommand(interp, "testMemIndirect", (createObjCmdProc testMemIndirect), IntPtr.Zero, ignoreDelete) |> ignore

// Set up GUI
let evalStr = """
wm title . {}
ttk::frame .f
ttk::button .f.testMemDirect -text {Test Direct} -command {testMemDirect}
ttk::button .f.testMemIndirect -text {Test Indirect} -command {testMemIndirect}
ttk::button .f.exit -text {Exit} -command {exit}

grid .f
grid .f.testMemDirect
grid .f.testMemIndirect
grid .f.exit
"""
tclEvalEx(interp, evalStr, evalStr.Length, 0) |> ignore

// Start Tk
tkMainLoop()
0 // return an integer exit code

我可以根据需要多次按“直接测试”按钮,没有任何问题。我可以毫无问题地按一次“测试间接”按钮。但是我第二次按下它,我得到:

Unhandled Exception: System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---> System.AccessViolationException: Attempted to read or write protected memory. This is often an indication that other memory is corrupt.

“Test Direct”按钮和“Test Indirect”按钮之间的区别在于,“Test Direct”使用 ObjCmdProc 委托调用 tclCreateObjCommand,而“Test Indirect”使用产生一个函数的函数调用 tclCreateObjCommand ObjCmdProc 代表。

我猜在“直接测试”的情况下,F# 固定ObjCmdProc 委托,因此没有内存损坏。但是,在“间接测试”的情况下,F# 正在移动委托的位置以响应垃圾回收。 (请注意,此问题仅在内存分配较大时才会出现 - 即,我正在生成一个大序列并将其转换为列表)。

假设我的猜测是正确的(可能不是),为什么 F# 会这样?更重要的是,有没有办法告诉 F# 不要移动这个函数在内存中的位置?我尝试了各种方法,例如GC.KeepAliveGCHandle.Alloc,但均未成功。


附录:

要解决这个问题,我所要做的就是保留对(createObjCmdProc testMemIndirect) 的引用。所以替换:

tclCreateObjCommand(interp, "testMemIndirect", (createObjCmdProc testMemIndirect), IntPtr.Zero, ignoreDelete) |> ignore

与:

let testMemIndirectRef = createObjCmdProc testMemIndirect
tclCreateObjCommand(interp, "testMemIndirect", testMemIndirectRef, IntPtr.Zero, ignoreDelete) |> ignore

否则,F# 认为该函数可以被垃圾回收。我可能还需要更改结束代码:

// Start Tk
tkMainLoop()
0 // return an integer exit code

到:

// Start Tk
tkMainLoop()
GC.KeepAlive(testMemDirect)
GC.KeepAlive(testMemIndirectRef)
0 // return an integer exit code

以确保代表在程序结束前一直保持活力。

请注意,此时我并不关心删除 Tcl 命令,即使在某些时候我可能会关心(正如该问题的答案正确指出的那样)。

【问题讨论】:

  • 一个有趣的问题!您正在集成两个不同的内存管理(和线程管理)系统,因此您需要非常小心。实际上,Tcl 中的所有内容(I/O 通道除外)都严格绑定到单个线程,这允许 Tcl 使用更快的内存分配器,因为它们可以是无锁的,但这确实意味着将托管系统与其集成可能有点挑战.你必须以“正确的方式”做事,否则你只会感到奇怪的痛苦。
  • 您可能会发现使用Eagle 更容易,这对于.NET 世界来说是有效的Tcl。我不认为它直接支持 Tk(我可能错了),但它解决了很多尴尬的问题,否则你会面临。
  • 我看了一眼 Eagle,但看不出有什么方法可以使用 Tk。我在 Eagle 网站上找不到声明不支持 Tk 的特定位置,但有一篇 2008 年的 .pdf 论文提到了它。我使用 Tcl 只是为了使用 Tk,所以我需要 Tk 支持。

标签: f# garbage-collection tcl tk memory-corruption


【解决方案1】:

在 C 级别,clientData 参数(您一直设置为 IntPtr.Zero)是您传递指向某些上下文的指针的方式,您需要正确调用实现命令的函数。没有它,你只是有一个指向函数的纯指针。因此,正确的做法是找到某种方法通过该机制传递fn;您需要保留对它的引用,直到注册的 cmdDeleteProc 被调用,并且该回调的主要任务是删除该 (owning!) 引用(每当删除命令时都会调用它从 Tcl 方面)。这就是 Tcl 的命令实现 API 的设计方式; 强烈建议您使用它。

我认为它在“直接”调用的情况下有效,因为 F# 中变量的引用使函数保持活动状态。你很幸运,在那种情况下它也没有炸毁你,因为它肯定仍然不安全。


我不知道如何通过原始指针将指针传递给 F# 函数实例。您可能需要将它放在某种普通的数据结构中,并传递一个指向它的指针。我也不知道如何告诉 F# 垃圾收集器,除非您另有说明(即,直到 Tcl 删除回调完成),否则无法收集该数据引用。但这些是你必须做的事情。您可能不希望将其细节直接暴露给您的大多数 F# 代码;连接器库是您只需编写一次,然后处理集成 .NET 和 Tcl 内存管理域的细节(这基本上是经典的 C,除了使用自定义内存分配器以提高速度) .

【讨论】:

  • .NET 框架会自动处理您的一些问题。例如,.NET 自动处理非托管代码调用委托。只要代表没有被垃圾回收link,它将代表固定在内存中。问题是(createObjCmdProc testMemIndirect) 正在收集垃圾,因为没有引用它。当 Tk 试图调用这个现在不存在的函数时,它崩溃了。如果我保留对该函数的引用,则它不会被垃圾收集。
  • 好吧,你需要一些方法来固定对委托的引用。删除回调将准确告诉您何时可以取消固定它,或者至少何时可以取消固定此特定提及。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-02-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-06-03
相关资源
最近更新 更多