-
-
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() |
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.
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
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.
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)