-
-
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
QtType
renamed 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
,ref
andptr
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 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
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)
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