【发布时间】: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.KeepAlive 和GCHandle.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