It is a bit hard to explain unfortunately. The problem are the GTK callbacks and signals. In C we can connect to a signal name given as a string providing a callback proc and a pointer to arbitrary data. In higher level languages we try to archive type safety, so we have to ensure that our provided data match our proc signature.
My nim-gi module (https://github.com/StefanSalewski/nim-gi2) can generate already some nice bindings (at least for Linux AMD64 and GTK 3.22), and with some help of Mr J. Mansour I created a first working test with a connect macro which looks like this:
proc clickd(button: XButton; arg: string) =
echo arg
echo button.x
# TODO: this macro will be moved to library module!
macro connect(widget: Widget; signal: string; p: typed; arg: typed): typed =
inc(ProcID)
let wt = getType(widget) # widget type
let at = getType(arg) # argument type
let signalName = ($signal).replace("-", "_") # maybe we should just use plain proc names
let procNameCdecl = newIdentNode("connect_for_signal_cdecl_" & signalName & $ProcID)
let procName = newIdentNode("connect_for_signal_" & signalName & $ProcID)
let scName = newIdentNode("sc" & signalName)
result = quote do:
proc `procNameCdecl`(button: ptr Object00 , data: pointer) {.cdecl.} =
var h: pointer = g_object_get_qdata(button, Quark)
`p`(cast[`wt`](h), cast[`at`](data))
proc `procName`(self: `wt`; p: proc (self: `wt`, arg: `at`); a: `at`) =
`scName`(self, `procNameCdecl`, cast[pointer](a))
`procName`(`widget`, `p`, `arg`)
and is called like
connect(button1, "clicked", clickd, "Hello")
(Full code at https://github.com/StefanSalewski/nim-gi2)
I am not sure if the macro has good shape, but it seems to work. Indeed it should not really work -- the passed in string "Hello" is collected by the GC at some time...
So what we need is a way to make that arbitrary typed data parameter persistent. For ref to objects a GC_ref would do. All other data which are not refs should need a copy which is then passed to the callback. So question is: How can I check if the parameter is a ref on which I can call GR_ref. And if not a ref, how can I do best a persistent copy, maybe a deep copy?
In best case it should be even possible to free that data later again, because GTK allows disconnecting of callbacks. But that is rarely used, and the data is generally small, so not much Ram is wasted.
Araq, my recent idea was to replace above code with
proc `procName`(self: `wt`; p: proc (self: `wt`, arg: `at`); a: `at`) =
when compiles(GC_ref(a)):
GC_ref(a)
`scName`(self, `procNameCdecl`, cast[pointer](a))
else:
var ar: ref `at`
new(ar)
deepCopy(ar[], a)
`scName`(self, `procNameCdecl`, cast[pointer](ar))
`procName`(`widget`, `p`, `arg`)
For this example the GC_ref() part is executed and GC_ref() crash. May it crash because a is a string literal and not a var? I hope at least the deepCopy part is OK.
[EDIT]
Well, the deepCopy parts indeed works, but I have to use "ar[]" instead of just "ar" to get the desired value 123. I will think about it more later...
proc clickd(button: XButton; arg: int) =
echo arg
echo button.x
proc `procName`(self: `wt`; p: proc (self: `wt`, arg: `at`); a: `at`) =
when false:#compiles(GC_ref(a)):
GC_ref(a)
`scName`(self, `procNameCdecl`, cast[pointer](a))
else:
var ar: ref `at`
new(ar)
deepCopy(ar[], a)
`scName`(self, `procNameCdecl`, cast[pointer](ar[]))
`procName`(`widget`, `p`, `arg`)
connect(button1, "clicked", clickd, 123)
Araq, the deepCopy() part works fine with string and int parameters, but unfortunately not with objects. I got it working, when I substitute at by "ParObj" literal everywhere: (Of course that makes no sense for general use case)
type
ParObj = object
x: float
proc clicked(button: XButton; arg: ParObj) =
echo arg.x
echo button.x
macro connect(widget: Widget; signal: string; p: untyped; arg: typed): typed =
inc(ProcID)
let wt = getType(widget) # widget type
let at = getType(arg) # argument type
let signalName = ($signal).replace("-", "_") # maybe we should just use plain proc names
let procNameCdecl = newIdentNode("connect_for_signal_cdecl_" & signalName & $ProcID)
let procName = newIdentNode("connect_for_signal_" & signalName & $ProcID)
let scName = newIdentNode("sc" & signalName)
result = quote do:
proc `procNameCdecl`(button: ptr Object00 , data: pointer) {.cdecl.} =
var h: pointer = g_object_get_qdata(button, Quark)
`p`(cast[`wt`](h), cast[ParObj](data))
#`p`(cast[`wt`](h), cast[`at`](data))
proc `procName`(self: `wt`; p: proc (self: `wt`, arg: ParObj); a: ParObj) =
when false:#compiles(GC_ref(a)):
GC_ref(a)
`scName`(self, `procNameCdecl`, cast[pointer](a))
else:
#var ar: ref `at`
var ar: ref ParObj
new(ar)
deepCopy(ar[], a)
`scName`(self, `procNameCdecl`, cast[pointer](ar[]))
`procName`(`widget`, `p`, `arg`)
connect(button1, "clicked", clicked, parObj)
So "let at = getType(arg)" in macro and using at is not identical to ParObj. While for base types int and strings it works. I have the feeling that getType() gives only plain object type. Is there a way to get real object type, which is ParObj here?
[EDIT]
What seems to work is, when I extract the real object type outside of the macro with type() and pass that typedesc as one more parameter to the macro. (Later I may use a template which calls the makro, so the additional parameter is not visible to the user.)
macro connect(widget: Widget; signal: string; p: untyped; arg: typed; rt: typedesc): typed =
connect(button1, "clicked", clicked, parObj, type(parObj))
Well, I have a working solution. I can pass a string, an int, and object from the stack to the callback and it works. (Removing a button and reinserting does not work currently, but that is a different problem.) But I still wonder why I have to use the strange name(type(arg)) way to get the type of the callback parameter, and getType(arg) in macro does not work. Of course this code always makes a deep copy of the callback parameter, that wasted some ram.
# plain test for high level gi based GTK3 Nim wrapper
# https://github.com/StefanSalewski/nim-gi2
import gtk, glib, gobject
import macros
import strutils
import typetraits
type
ParObj = object
x: float
type
XButton = ref object of Button
x: int
# TODO: will be moved to library module!
let Quark = g_quark_from_static_string("NimGIQuark")
# TODO: will be moved to library module!
proc initWithArgv*() =
var
cmdLine{.importc.}: cstringArray
cmdCount{.importc.}: cint
gtk.gtk_init(cmdCount, cmdLine)
proc clicked(button: XButton; arg: ParObj) =
echo arg.x
echo button.x
proc clicked2(button: Button; arg: string) =
echo arg
proc clicked3(button: Button; arg: int) =
echo arg
proc bye(w: Window; arg: string) =
mainQuit()
echo arg
var ProcID: int
# TODO: this macro will be moved to library module!
macro mconnect(widget: Widget; signal: string; p: untyped; arg: typed; rrt: string): typed =
inc(ProcID)
let wt = getType(widget) # widget type
#let at = getType(arg) # argument type
let signalName = ($signal).replace("-", "_") # maybe we should just use plain proc names
let procNameCdecl = newIdentNode("connect_for_signal_cdecl_" & signalName & $ProcID)
let procName = newIdentNode("connect_for_signal_" & signalName & $ProcID)
let scName = newIdentNode("sc" & signalName)
let at = newIdentNode($rrt)
result = quote do:
proc `procNameCdecl`(button: ptr Object00 , data: pointer) {.cdecl.} =
var h: pointer = g_object_get_qdata(button, Quark)
`p`(cast[`wt`](h), cast[`at`](data))
proc `procName`(self: `wt`; p: proc (self: `wt`, arg: `at`); a: `at`) =
var ar: ref `at`
new(ar)
deepCopy(ar[], a)
GC_ref(ar)
`scName`(self, `procNameCdecl`, cast[pointer](ar[]))
`procName`(`widget`, `p`, `arg`)
template connect(widget: Widget; signal: string; p: untyped; arg: typed) =
mconnect(widget, signal, p, arg, name(type(arg)))
proc work =
var window: Window = newWindow(WindowType.topLevel)
window.setTitle("First Test")
window.setBorderWidth(10)
var
box = newBox(Orientation.vertical, 0)
button1: XButton
button2 = newButton("Wrapper")
button3 = newButton("Int button")
parObj: ParObj
parObj.x = 3.1415
initButton(button1)
button1.setLabel("Nim GI")
button1.x = 99
connect(button1, "clicked", clicked, parObj)
connect(button3, "clicked", clicked3, 1234567)
connect(window, "destroy", bye, "Bye")
button2.connect("clicked", (proc(button: Button; arg: string) = echo arg), "Bye")
box.add(button1)
box.add(button2)
box.add(button3)
#box.remove(button2) # this does not work currently
#box.add(button2)
window.add(box)
window.showAll
let p = button1.getParent
assert(p == box)
proc main() =
initWithArgv()
work()
GC_fullCollect()
gtk.gtkMain()
main()
I tried to improve the above first draft. With the hints in https://stackoverflow.com/questions/38029742/nim-reflect-on-types-field-types-at-compile-time I got the following working code. It does not require typetraits any longer and is more type safe: If arguments of the clicked procs do not match, I seems to get indeed compile time errors as desired. I found no way to avoid the additional template. It is required to get the typedescs and pass them to the macro. (Garbage collector seems to work also fine with the GTK widgets now. But of course I still have to do much testing.)
# plain test for high level gi based GTK3 Nim wrapper
# https://github.com/StefanSalewski/nim-gi2
import gtk, glib, gobject
import macros
import strutils
#import typetraits
type
ParObj = object
x: float
type
XButton = ref object of Button
x: int
# TODO: will be moved to library module!
let Quark = g_quark_from_static_string("NimGIQuark")
# TODO: will be moved to library module!
proc initWithArgv*() =
var
cmdLine{.importc.}: cstringArray
cmdCount{.importc.}: cint
gtk.gtk_init(cmdCount, cmdLine)
proc clicked(button: XButton; arg: ParObj) =
echo arg.x
echo button.x
proc clicked2(button: Button; arg: string) =
echo arg
proc clicked3(button: Button; arg: int) =
echo arg
proc bye(w: Window; arg: string) =
mainQuit()
echo arg
var ProcID: int
# TODO: this macro will be moved to library module!
macro mconnect(widget: Widget; signal: string; p: untyped; arg: typed; widgett: typedesc; argt: typedesc): typed =
inc(ProcID)
let wt = getType(widgett)[1] # widget type
let at = getType(argt)[1] # argument type
echo treerepr(wt)
echo treerepr(at)
let signalName = ($signal).replace("-", "_") # maybe we should just use plain proc names
let procNameCdecl = newIdentNode("connect_for_signal_cdecl_" & signalName & $ProcID)
let procName = newIdentNode("connect_for_signal_" & signalName & $ProcID)
let scName = newIdentNode("sc" & signalName)
result = quote do:
proc `procNameCdecl`(button: ptr Object00 , data: pointer) {.cdecl.} =
var h: pointer = g_object_get_qdata(button, Quark)
`p`(cast[ref `wt`](h), cast[`at`](data))
proc `procName`(self: ref `wt`; p: proc (self: ref `wt`, arg: `at`); a: `at`) =
var ar: ref `at`
new(ar)
deepCopy(ar[], a)
GC_ref(ar)
`scName`(self, `procNameCdecl`, cast[pointer](ar[]))
`procName`(`widget`, `p`, `arg`)
template connect(widget: Widget; signal: string; p: untyped; arg: typed) =
mconnect(widget, signal, p, arg, type(widget[]), type(arg))
proc work =
var window: Window = newWindow(WindowType.topLevel)
window.setTitle("First Test")
window.setBorderWidth(10)
var
box = newBox(Orientation.vertical, 0)
button1: XButton
button2 = newButton("Wrapper")
button3 = newButton("Int button")
button4 = newButton("Newer used")
parObj: ParObj
parObj.x = 3.1415
initButton(button1)
button1.setLabel("Nim GI")
button1.x = 99
connect(button1, "clicked", clicked, parObj)
connect(button3, "clicked", clicked3, 1234567)
connect(window, "destroy", bye, "Bye")
button2.connect("clicked", (proc(button: Button; arg: string) = echo arg), "Bye")
box.add(button1)
box.add(button2)
box.add(button3)
echo "removing button2"
box.remove(button2)
echo "and add again"
box.add(button2)
echo "done"
echo "removing button2"
box.remove(button2)
echo "and add again"
box.add(button2)
echo "done"
assert(button4 != nil)
window.add(box)
window.showAll
let p = button1.getParent
assert(p == box)
proc main() =
initWithArgv()
work()
GC_fullCollect()
gtk.gtkMain()
main()