Skip to content

Instantly share code, notes, and snippets.

@jovial
Last active August 29, 2015 14:12
Show Gist options
  • Save jovial/f99380b6e975b2b60f27 to your computer and use it in GitHub Desktop.
Save jovial/f99380b6e975b2b60f27 to your computer and use it in GitHub Desktop.
Non working Qt macro
import NimQml
import macros
import typeinfo
import strutils
import typetraits
import tables
static:
let nimFromQtVariant = {
"int" : "intVal",
"string" : "stringVal",
"bool" : "boolVal" ,
}.toTable
let nim2QtMeta = {
"bool": "Bool",
"int " : "Int",
"string" : "QString",
"pointer" : "VoidStar",
"QVariant": "QVariant",
"" : "Void", # return like will be an EmptyNode
}.toTable
proc getNodeOf*(a: PNimrodNode, b: TNimrodNodeKind): PNimrodNode {.compileTime.} =
for i in 0.. <a.len:
var child = a[i]
if child of PNimrodNode and child.kind == b:
return child
var candidate = getNodeOf(child, b)
if not candidate.isNil:
return candidate
static:
type Context* = ref object of RootObj
type NullContext* = ref object of Context
type NodeModifier*[T] = proc(context: T, a: var PNimrodNode): PNimrodNode
# had to remove type bound on hook due to recent regression with generics
proc hookOnNode*[T](context: T, code: PNimrodNode, hook: NodeModifier, recursive: bool = false): PNimrodNode {.compileTime.} =
if code.len == 0:
return code
var newCode = newNimNode(code.kind)
for i in 0.. <code.len:
var child = code[i].copy()
child = hook(context, child)
if recursive:
child = hookOnNode(context,child,hook,true)
if child != nil:
newCode.add child
return newCode
proc removeOpenSym*(context: NullContext, a: var PNimrodNode): PNimrodNode {.compileTime.} =
if a.kind == nnkOpenSymChoice:
return ident($a[0].symbol)
elif a.kind == nnkSym:
return ident($a.symbol)
return a
proc newTemplate*(name = newEmptyNode(); params: openArray[PNimrodNode] = [newEmptyNode()];
body: PNimrodNode = newStmtList()): PNimrodNode {.compileTime.} =
## shortcut for creating a new template
##
## The ``params`` array must start with the return type of the template,
## followed by a list of IdentDefs which specify the params.
result = newNimNode(nnkTemplateDef).add(
name,
newEmptyNode(),
newEmptyNode(),
newNimNode(nnkFormalParams).add(params), ##params
newEmptyNode(), ## pragmas
newEmptyNode(),
body)
template declareSuperTemplate*(parent: typedesc, typ: typedesc): typedesc =
template superType*(ofType: typedesc[typ]): typedesc[parent] =
parent
proc getTypeName*(a: PNimrodNode): PNimrodNode {.compileTime.} =
expectMinLen a, 1
expectKind a, nnkTypeDef
var testee = a
if testee[0].kind == nnkPragmaExpr:
testee = testee[0]
if testee[0].kind in {nnkIdent}:
return testee[0]
elif testee[0].kind in {nnkPostfix}:
return testee[0][1]
proc genSuperTemplate*(typeDecl: PNimrodNode): PNimrodNode {.compileTime.} =
expectKind typeDecl, nnkTypeDef
let inheritStmt = typeDecl.getNodeOf(nnkOfInherit)
let typeName = getTypeName(typeDecl)
if inheritStmt == nil: error("you must declare a super type for " & $typeName)
# ident of superType (have to deal with generics)
let superType = if inheritStmt[0].kind == nnkIdent: inheritStmt[0] else: inheritStmt[0].getNodeOf(nnkIdent)
let superTemplate = getAst declareSuperTemplate(superType, typeName)
return superTemplate[0]
proc getSuperType*(typeDecl: PNimrodNode): PNimrodNode {.compileTime.} =
## returns ast containing superType info, may not be an ident if generic
let inheritStmt = typeDecl.getNodeOf(nnkOfInherit)
if inheritStmt.isNil: return newEmptyNode()
return inheritStmt[0]
proc getPragmaName*(child: PNimrodNode): PNimrodNode {.compileTime.} =
## name of child in a nnkPragma section
if child.kind == nnkIdent:
return child
# assumes first ident is name of pragma
let ident = child.getNodeOf(nnkIdent)
result = ident
proc removePragma*(pragma: PNimrodNode, toRemove: string): PNimrodNode {.compileTime.} =
expectKind pragma, nnkPragma
result = newNimNode(nnkPragma)
for i in 0.. <pragma.len:
let child = pragma[i]
if $child.getPragmaName == toRemove:
continue
result.add child
if result.len == 0:
return newEmptyNode()
proc hasPragma*(node: PNimrodNode, pragmaName: string): bool {.compileTime.} =
doAssert node.kind in {nnkMethodDef, nnkProcDef}
result = false
let pragma = node.pragma
if pragma.kind == nnkEmpty:
# denotes no pragma set
return false
for child in pragma.children():
if $child.getPragmaName() == pragmaName:
return true
proc getArgType*(arg: PNimrodNode): PNimrodNode {.compileTime.} =
if arg[1].kind == nnkIdent:
arg[1]
else:
arg[1].getNodeOf(nnkIdent)
proc getArgName*(arg: PNimrodNode): PNimrodNode {.compileTime.} =
if arg[0].kind == nnkIdent:
arg[0]
else:
arg[0].getNodeOf(nnkIdent)
proc addSignalBody(signal: PNimrodNode): PNimrodNode {.compileTime.} =
# e.g: produces: emit(MyQObject, "nameChanged")
expectKind signal, nnkMethodDef
result = newStmtList()
# if exported, will use postfix
let name = if signal.name.kind == nnkIdent: signal.name else: signal.name[1]
let params = signal.params
# type signal defined on is the 1st arg
let self = getArgName(params[1])
var args = newSeq[PNimrodNode]()
args.add(self)
args.add newLit($name)
if params.len > 2: # more args than just type
for i in 2.. <params.len:
args.add getArgName params[i]
result.add newCall("emit", args)
template declareOnSlotCalled(typ: typedesc): stmt =
method onSlotCalled(myQObject: typ, slotName: string, args: openarray[QVariant]) =
discard
template prototypeCreate(typ: typedesc): stmt =
template create(myQObject: var typ) =
var super = (typ.superType())(myQObject)
procCall create(super)
proc doRemoveOpenSym(a: var PNimrodNode): PNimrodNode {.compileTime.} =
hookOnNode(NullContext(),a, removeOpenSym, true)
proc templateBody(a: PNimrodNode): PNimrodNode {.compileTime.} =
expectKind a, nnkTemplateDef
result = a[6]
proc genArgTypeArray(params: PNimrodNode): PNimrodNode {.compileTime.} =
expectKind params, nnkFormalParams
result = newNimNode(nnkBracket)
for i in 0 .. <params.len:
if i == 1:
# skip "self" param eg: myQObject: MyQObject
continue
let pType = if i != 0: getArgType params[i] else: params[i]
let pTypeString = if pType.kind == nnkEmpty: "" else: $pType
# function that maps Qvariant type to nim type
let qtMeta = nim2QtMeta[pTypeString]
if qtMeta == nil: error(pTypeString & " not supported yet")
let metaDot = newDotExpr(ident "QMetaType", ident qtMeta)
result.add metaDot
proc getIdentDefName(a: PNimrodNode): PNimrodNode {.compileTime.} =
expectKind a, nnkIdentDefs
if a[0].kind == nnkIdent:
return a[0]
elif a[0].kind == nnkPostFix:
return a[0][1]
macro QtType(qtDecl: stmt): stmt {.immediate.} =
expectKind(qtDecl, nnkStmtList)
#echo treeRepr qtDecl
result = newStmtList()
var slots = newSeq[PNimrodNode]()
var properties = newSeq[PNimrodNode]()
var signals = newSeq[PNimrodNode]()
# assume only one type per section for now
var typ: PNimrodNode
for it in qtDecl.children():
if it.kind == nnkTypeSection:
# add the typesection unchanged
let typeDecl = it.findChild(it.kind == nnkTypeDef)
let superType = typeDecl.getSuperType()
if superType.kind != nnkEmpty:
let superName = if superType.kind == nnkIdent: superType else: superType.getNodeOf(nnkIdent)
if $superName != "QtProperty":
if typ != nil:
error("only support one type declaration")
typ = typeDecl.getTypeName
result.add it
result.add genSuperTemplate(typeDecl)
else:
# process later
properties.add(it)
elif it.kind == nnkMethodDef:
if it.hasPragma("slot"):
let pragma = it.pragma()
it.pragma = pragma.removePragma("slot")
slots.add it # we need to gensome code later
result.add it
elif it.hasPragma("signal"):
let pragma = it.pragma()
it.pragma = pragma.removePragma("signal")
it.body = addSignalBody(it)
result.add it
signals.add it
## define onSlotCalled
var slotProto = (getAst declareOnSlotCalled(typ))[0]
var caseStmt = newNimNode(nnkCaseStmt)
caseStmt.add ident("slotName")
for slot in slots:
var ofBranch = newNimNode(nnkOfBranch)
# for exported procedures - strip * marker
let slotName = ($slot.name).replace("*","")
ofBranch.add newLit slotName
let params = slot.params
let hasReturn = not (params[0].kind == nnkEmpty)
var branchStmts = newStmtList()
let paramCount = params.len -1 # don't include ret (arg 0)
var args = newSeq[PNimrodNode]()
# first params always the object
args.add ident "myQObject"
for i in 2.. <params.len:
let pType = getArgType params[i]
# function that maps Qvariant type to nim type
let mapper = nimFromQtVariant[$pType]
let argAccess = newNimNode(nnkBracketExpr)
.add (ident "args")
.add newIntLitNode(i-1)
let dot = newDotExpr(argAccess, ident mapper)
args.add dot
var call = newCall(ident slotName, args)
if hasReturn:
# eg: args[0].strVal = getName(myQObject)
let retType = params[0]
let mapper = nimFromQtVariant[$retType]
let argAccess = newNimNode(nnkBracketExpr)
.add (ident "args")
.add newIntLitNode(0)
let dot = newDotExpr(argAccess, ident mapper)
call = newAssignment(dot, call)
branchStmts.add call
ofBranch.add branchStmts
caseStmt.add ofBranch
# add else: discard
caseStmt.add newNimNode(nnkElse)
.add newStmtList().add newNimNode(nnkDiscardStmt).add newNimNode(nnkEmpty)
slotProto.body = newStmtList().add caseStmt
result.add slotProto
# generate create method
var createProto = (getAst prototypeCreate(typ))[0]
# the template creates loads of openSyms - replace these with idents
createProto = doRemoveOpenSym(createProto)
var createBody = createProto.templateBody
for slot in slots:
let params = slot.params
let regSlotDot = newDotExpr(ident "myQObject", ident "registerSlot")
let name = ($slot.name).replace("*","")
let argTypesArray = genArgTypeArray(params)
let call = newCall(regSlotDot, newLit name, argTypesArray)
createBody.add call
for signal in signals:
let params = signal.params
let regSigDot = newDotExpr(ident "myQObject", ident "registerSignal")
let name = ($signal.name).replace("*","")
let argTypesArray = genArgTypeArray(params)
let call = newCall(regSigDot, newLit name, argTypesArray)
createBody.add call
for property in properties:
let inherit = property.getNodeOf(nnkOfInherit)
# OfInherit
# BracketExpr
# Ident !"QtProperty"
# Ident !"string"
let nimPropType = inherit[0][1]
let qtPropMeta = nim2QtMeta[$nimPropType]
if qtPropMeta == nil: error($nimPropType & " not supported")
let metaDot = newDotExpr(ident "QMetaType", ident qtPropMeta)
let typeDef = property.getNodeOf(nnkTypeDef)
let typeName = typeDef.getTypeName()
var read, write, notify: PNimrodNode
# fields
let recList = typeDef.getNodeof(nnkRecList)
for identDef in recList.children:
let name = identDef.getIdentDefName()
case $name
of "read":
read = identDef[2]
of "write":
write = identDef[2]
of "notify":
notify = identDef[2]
else:
error("unknown property field: " & $name)
let regPropDot = newDotExpr(ident "myQObject", ident "registerProperty")
let readArg = if read == nil: newNilLit() else: newLit($read)
let writeArg = if write == nil: newNilLit() else: newLit($write)
let notifyArg = if notify == nil: newNilLit() else: newLit($notify)
let call = newCall(regPropDot, newLit($typeName), metaDot, readArg, writeArg, notifyArg)
createBody.add call
#echo repr createProto
result.add createProto
echo repr result
QtType:
type MyQObject = ref object of QObject
m_name: string
method getName(myQObject: MyQObject): string {.slot.} =
result = myQObject.m_name
method nameChanged(myQObject: MyQObject) {.signal.}
method setName(myQObject: MyQObject, name: string) {.slot.} =
if myQObject.m_name != name:
myQObject.m_name = name
myQObject.nameChanged()
type name = object of QtProperty[string]
read = getName
write = setName
notify = nameChanged
proc mainProc() =
var app: QApplication
app.create()
defer: app.delete()
var myQObject = MyQObject()
myQObject.create()
defer: myQObject.delete()
myQObject.m_name = "InitialName"
var engine: QQmlApplicationEngine
engine.create()
defer: engine.delete()
var variant: QVariant
variant.create(myQObject)
defer: variant.delete()
var rootContext: QQmlContext = engine.rootContext()
rootContext.setContextProperty("myQObject", variant)
engine.load("main.qml")
app.exec()
when isMainModule:
mainProc()
@filcuc
Copy link

filcuc commented Jan 1, 2015

mmm i don't get what onSlotCalled method is called..seems that nor the generated nor the one of my QObject base class is called

@jovial
Copy link
Author

jovial commented Jan 2, 2015

Yeah, at the moment I have no idea why that happens. If anybody knows the reason, It would be good to hear 😄

I believe semi colons are legal separators for parameters (I vaguely recall Araq saying they were preferred - but maybe I am wrong on this)

@jovial
Copy link
Author

jovial commented Jan 2, 2015

I updated the code to use a template. The code gen bug happens when create is either a proc or a method. The work around was just satisfying the c compiler, so it may have been causing some other corruption.

@filcuc
Copy link

filcuc commented Jan 2, 2015

I tested The Last version and everything seems working fine! I think we will integrate this as a new module called NimQmlMacros. For the next version i'my planning to insert your work and write some basic documentation and improve the current examples

@jovial
Copy link
Author

jovial commented Jan 2, 2015

Sounds good. Do you want me to move it to the new module? If so:

  • do you want QtType renamed to QtObject?
  • shall I implement the syntax you suggested for properties on the forum?

Possible improvements for the QtType macro:

  • only export generated templates/procedures/methods when the object deriving from QObject is exported (i.e marked with '*')
  • need to check it works with var, ref and ptr parameters in signal/slots definitions.
  • make sure create can be called from a user defined function within the block passed to the macro, e.g if someone wants to define a constructor which calls create, and sets the fields. We can either use a forward-definition or move all user defined procedures/methods to a place after the generated functions.
  • Any more you can think of?

I also thought about how we could handle generics like:

type A[T] = ref object of QObject
  (fields omitted)
type B[T] = ref object of A[T]
  (fields omitted)

one solution is:

  • in the macro, create a compileTime proc that returns the generic base type as a string. E.g, for B[T] some thing like:
proc genericSuperAsString[T](a: B[T]): string = "A[$1]" % [T.type.name]

for, x = B[string], this would return "A[string]".

  • then generate a helper macro that converts the string to a typedesc.
  • the create method would also have to made generic
  • unsure if any other problems would arise at this point (e.g could we have generic properties)

why this is necessary:

  • can only return a typedesc from a template or macro
  • templates and macros cannot be generic
  • current method is to use a non generic template, which wouldn't work when generics are involved

By the way, I narrowed down the cause of the codegen bug: see nim-lang/Nim#1821. I also have a feature request for an inbuilt superType procedure: nim-lang/Nim#1719, which would make things easier 😄

Edit: scrap that generic idea - I don't think it would work (no way to pass a string to a macro)

@filcuc
Copy link

filcuc commented Jan 3, 2015

I think that for the initial time we can live without generic QObject. I really think that we need support from the core Nim dev (a "super" statement is necessary IMHO in a language with OOP). You've done a great work there.

For the QtType renamed i think that QtObject fits better, my hey it's just my own opinion :)

For the documentation i started a new feature branch on my repo. I need also to write some roadmap documentation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment