-
-
Save jovial/f99380b6e975b2b60f27 to your computer and use it in GitHub Desktop.
| 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() |
Sounds good. Do you want me to move it to the new module? If so:
- do you want
QtTyperenamed toQtObject? - 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,refandptrparameters in signal/slots definitions. - make sure
createcan be called from a user defined function within the block passed to the macro, e.g if someone wants to define a constructor which callscreate, 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
typedescfrom 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)
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.
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