I thought maybe someone would find this interesting: the following macro lets you to define functions and containers that accept objects of any type, as long as they have a desired set of fields. The objects may have additional fields, and the order of the fields in the type definition doesn't matter. It's different from generics in that it allows for runtime polymorphism. There's a usage example at the bottom. It's quite cool that Nim allows you to do stuff like this.
import macros
import os
{.experimental.}
proc `-`(a: pointer, b: pointer): int =
cast[int](a) - cast[int](b)
proc `+`(a: pointer, b: int): pointer =
cast[pointer](cast[int](a) + cast[int](b))
macro structural(con, inter, fieldDefs): untyped =
let dummySym = genSym(nskVar, "dummy")
let vtableType = newNimNode(nnkTupleTy)
let vtableValRef = newNimNode(nnkPar)
let vtableValPtr = newNimNode(nnkPar)
let conTests = newNimNode(nnkStmtList)
for fieldDef in fieldDefs.children:
# Build up vtable tuple type and value, as well as concept tests, from the fields:
let fname = fieldDef[0]
let ftype = fieldDef[1][0]
var a = newNimNode(nnkIdentDefs)
a.add(fname)
a.add(quote do: tuple[offs: int, tmark: `ftype`])
a.add(newEmptyNode())
vtableType.add(a)
var b = newNimNode(nnkExprColonExpr)
b.add(fname)
b.add(quote do: (addr(`dummySym`.`fname`) - addr(`dummySym`[]), `dummySym`.`fname`))
vtableValRef.add(b)
var c = newNimNode(nnkExprColonExpr)
c.add(fname)
c.add(quote do: (addr(`dummySym`.`fname`) - addr(`dummySym`), `dummySym`.`fname`))
vtableValPtr.add(c)
conTests.add(quote do: T.`fname` is `ftype`)
let node = quote do:
type
`con` = concept g, type T
`conTests`
`inter`[T: pointer or RootRef] = object
base: T
vtable: ptr[`vtableType`]
# Converter for ref object types:
# (typedesc is explicitly ruled out to prevent infinite loops in the concept tests)
converter conToInter[T: not typedesc and ref object and `con`](x: T): `inter`[RootRef] =
var vt {.global.} = (proc(): `vtableType` =
var `dummySym` = T()
return `vtableValRef`
)()
`inter`[RootRef](
# Questionable cast, but sizeof and repr suggest refs are glorified pointers anyway:
base: cast[RootRef](x), # Keeps the proxied object alive, provides base address
vtable: addr vt
)
# Converter for ptr object types:
converter conToInter[T: not typedesc and object and `con`](x: ptr T): `inter`[pointer] =
var vt {.global.} = (proc(): `vtableType` =
var `dummySym` = T()
return `vtableValPtr`
)()
`inter`[pointer](
base: x,
vtable: addr vt
)
let node2 = quote("@") do:
# Syntax sugar for referring to target fields through the proxy
# This must be separated from the first quote due to issues with the backtick splice
# Uses the vtable's dummy tmarks to cast to the correct field type
template `.`(x: @inter, field: untyped): untyped =
cast[ptr[(@vtableType).field.tmark]](cast[pointer](x.base) + x.vtable.field.offs)[]
template `.=`(x: @inter, field: untyped, value: untyped): untyped =
cast[ptr[(@vtableType).field.tmark]](cast[pointer](x.base) + x.vtable.field.offs)[] = value
let node3 = newStmtList()
node3.add(node)
node3.add(node2)
node3
# ---------------------------------------
# Usage example:
type
Vec2 = tuple[x, y: float]
Box = object
pos: Vec2
vel: Vec2
Circle = object
vel: Vec2
pos: Vec2
rad: float
BoxRef = ref Box
CircleRef = ref Circle
Turtle = ref object
speed: float
pos: Vec2
structural(Kinetic, IKinetic): # Magic happens here
pos: Vec2
vel: Vec2
var ptrSeq: seq[IKinetic[pointer]] = @[]
var refSeq: seq[IKinetic[RootRef]] = @[]
proc test1(x: IKinetic[RootRef]) =
echo x.pos
echo x.vel
refSeq.add(x)
proc test2(x: IKinetic[pointer]) =
echo x.pos
echo x.vel
ptrSeq.add(x)
test1(BoxRef(vel: (1.0, 2.0), pos: (3.0, 4.0)))
test1(CircleRef(vel: (5.0, 6.0), pos: (7.0, 8.0), rad: 125.0))
test2(cast[ptr Circle](alloc0(sizeof Circle)))
echo refSeq[1].vel
echo ptrSeq[0].vel
#test1(Turtle()) # Gives "Kinetic: concept predicate failed" error
Very cool!
(Still ... can't wait for concepts + vtrefs! ;) )
vtref is virtual table ref.
I don't know much about C++, what's the advantage of virtual table?
vtref is virtual table ref.
I think what I did is already a virtual table ref of sorts, so I'm not sure what boia01 meant. Maybe that something like this (or better) is already being planned for the Nim core? That's always preferable to improvised solutions, I guess.
what's the advantage of virtual table?
Over what?
This is not my work, butif you are interested, I had already collected something by @Krux02 similar to your work and published as a nimble package, now improved by @RSDuck