This is not a question, more like a blog post. I don't have a blog so I do it
here. Feel free to comment or ask questions.
In my past I have learned the programming language Go, and I really started to
like the interface type that Go offers, and I wondered, weather it is possible
to port this go languge feature to Nim with the aid of macros.
First of all, I would like to show you a bit of the advangae of this Go
interface type, by comparing it the the opetions c++ can give you.
_virtual methods_
struct MyInterface {
virtual int foo() = 0;
virtual int bar() = 0;
virtual ~MyInterface(){}
};
struct MyImplementationA: public MyInterface {
int foo() override { return 17; }
int bar() override { return 4; }
};
struct MyImplementationB: public MyInterface {
int m_foo;
int m_bar;
int foo() override { return m_foo; }
int bar() override { return m_bar; }
};
// polymorphic usage
int foobar(MyInterface* mi) {
return mi->foo() + mi->bar();
}
#include <iostream>
using std::cout;
using std::endl;
int main() {
MyImplementationA a;
MyImplementationB b;
b.m_foo = 32;
b.m_bar = 8;
cout << foobar(&a) << endl;
cout << foobar(&b) << endl;
}
In this version the virtual method dispatch in foobar is done with the vtable.
With the declaration of the first virtual method in MyInterface c++ adds a
hidden field to the struct, the so called vtable pointer. that points to a
struct with method pointers to the method implementations. Both
MyImplementationA and MyImplementationB change the value of this pointer to the
vtable of teir type. The method invocation foo in foobar opens the vtable, and
reads from there which method to invoke. Dependent on the type it is either
MyImplementationA::foo or MyImplementationB::foo. Because of this explicit
inheritance the interface implementation needs to be newer than the interface
itself. Also small objects that need to be compact in memory should better not
have virtual methods because of this added hidden field. C# and Java work
almost identical, just that in java everything is virtual.
_templates_
struct MyImplementationA {
int foo() { return 17; }
int bar() { return 4; }
};
struct MyImplementationB {
int m_foo;
int m_bar;
int foo() { return m_foo; }
int bar() { return m_bar; }
};
// polymorphic usage
template <typename T>
int foobar(T* mi) {
return mi->foo() + mi->bar();
}
#include <iostream>
using std::cout;
using std::endl;
int main() {
MyImplementationA a;
MyImplementationB b;
b.m_foo = 32;
b.m_bar = 8;
cout << foobar(&a) << endl;
cout << foobar(&b) << endl;
}
This version beats all problems of the previous version, but not without
introducing new problems. There is no hidden field in neither MyImplementationA
nor MyImplementationB. foobar is compiled for each type it is called
separately. Meaning that the compiler can also inline the the content from foo
and bar. Templates in c++ have a huge problem of being recompiled not only for
each type, but also for each compilation unit. So depending on the complexity
of foobar and the amount of compilation units where foobar is used, this
version can really explode compilation time in bigger problems. This is so bad
that several bigger c++ projects completely abandoned templates. Nim generics
are very much like c++ templates, so I guess they have this flaw as well. I
just don't have big enough projects yet to notice any major concerns with Nim's
generics.
_Go Interface_
package main
import "fmt"
type MyImplementationA struct {}
func (this *MyImplementationA) foo() int {
return 17;
}
func (this *MyImplementationA) bar() int {
return 4;
}
type MyImplementationB struct {
m_foo,m_bar int
}
type MyInterface interface {
foo() int
bar() int
}
func (this *MyImplementationB) foo() int {
return this.m_foo;
}
func (this *MyImplementationB) bar() int {
return this.m_bar;
}
// polymorphic usage
func foobar(mi MyInterface) int {
return mi.foo() + mi.bar();
}
func main() {
a := MyImplementationA{}
b := MyImplementationB{32, 8}
fmt.Println(foobar(&a))
fmt.Println(foobar(&b))
}
The Go version does neither put a hidden field in the struct datatype or mark
anything as virtual. In fact virtual is not a keyword in go nor does it have an
equivalent. Methods just become virtual, as soon as the type gets casted into
an interface type.
This works, because the vtable is not part of the struct. An instance of
MyInterface is actually a tuple of two pointers. The first one is the vtable
pointer, and the second one is the pointer to the struct that implements the
interface. With this design decision the Go compiler can just create a vtable
for any type that has the methods required to implement it. And when the method
foobar grows over time, and get's used by a lot of different types, it still
get's compiled only once. If you remember one of Go's main features is it's
compilation speed.
Now I want the same concept of the Go interfaces in Nim. The idea is to do the
same thing in two steps:
> 1. build an example case with everything, including vtable and iterface
> written manually.
> 2. write macros that create the exact same vtable and interface that were
> manually created in step 1
>
To not make this post too long, I have joined thise two steps into one step. A
macro call is in this code followed by a multiline comment that exactly
represents the generated source code
import interfacemacros
type
MyImplementationA = object
data : array[0,byte]
MyImplementationB = object
foo : int
bar : int
createInterface(MyInterface):
proc foo(this : MyInterface) : int
proc bar(this : MyInterface) : int
#[
type
MyInterfaceVtable = object
foo: proc (this: pointer): int
bar: proc (this: pointer): int
MyInterface = object
objet: pointer
vtable: ptr MyInterfaceVtable
proc foo(this: MyInterface): int =
this.vtable.foo(this.objet)
proc bar(this: MyInterface): int =
this.vtable.bar(this.objet)
]#
proc foo(this : ptr MyImplementationA) : int = 17
proc bar(this : ptr MyImplementationA) : int = 4
implementInterface(MyInterface, MyImplementationB)
#[
let MyImplementationBVtable = MyInterfaceVtable(
foo: proc (this: pointer): int = cast[ptr MyImplementationB](this).foo,
bar: proc (this: pointer): int = cast[ptr MyImplementationB](this).bar
)
proc MyInterface_InterfaceCast(this: ptr MyImplementationB): MyInterface =
MyInterface(objet: this, vtable: MyImplementationBVtable.unsafeAddr)
]#
implementInterface(MyInterface, MyImplementationA)
#[
let MyImplementationAVtable = MyInterfaceVtable(
foo: proc (this: pointer): int = cast[ptr MyImplementationA](this).foo,
bar: proc (this: pointer): int = cast[ptr MyImplementationA](this).bar
)
proc MyInterface_InterfaceCast(this: ptr MyImplementationA): MyInterface =
MyInterface(objet: this, vtable: MyImplementationAVtable.unsafeAddr)
]#
proc foobar(mi: MyInterface) : int =
return mi.foo + mi.bar
var
a : MyImplementationA
b : MyImplementationB = MyImplementationB(foo : 32, bar : 8)
echo foobar(MyInterface_InterfaceCast(a.addr))
echo foobar(MyInterface_InterfaceCast(b.addr))
The differences two the Go version are:
> * The interface declaration is in a macro call.
> * The vtables for the different types are requested explicitly with
> implementInterface(MyInterface, MyImplementation)
> * The conversion between pointer type and interface type is explicit with
> MyInterface_InterfaceCast
> * error messages when the interface can not be implemented are not really
> _nice_
>
And for completion, here are the macros that do the magic:
import macros
macro createInterface*(name : untyped, methods : untyped) : untyped =
name.expectKind nnkIdent
let
vtableRecordList = nnkRecList.newTree
vtableIdent = newIdentNode($name.ident & "Vtable")
vtableTypeDef = nnkTypeSection.newTree(
nnkTypeDef.newTree(
vtableIdent,
newEmptyNode(),
nnkObjectTy.newTree(
newEmptyNode(),
newEmptyNode(),
vtableRecordList
)
)
)
var newMethods = newSeq[NimNode]()
for meth in methods:
meth.expectKind(nnkProcDef)
let
methodIdent = meth[0]
params = meth[3]
thisParam = params[1]
thisIdent = thisParam[0]
thisType = thisParam[1]
if thisType != name:
error thisType.repr & " != " & name.repr
let vtableEntryParams = params.copy
vtableEntryParams[1][1] = newIdentNode("pointer")
vtableRecordList.add(
nnkIdentDefs.newTree(
methodIdent,
nnkProcTy.newTree(
vtableEntryParams,
newEmptyNode(),
),
newEmptyNode()
)
)
let call = nnkCall.newTree(
nnkDotExpr.newTree( nnkDotExpr.newTree(thisIdent,
newIdentNode("vtable")), methodIdent ),
nnkDotExpr.newTree( thisIdent, newIdentNode("objet") ),
)
for i in 2 ..< len(params):
let param = params[i]
param.expectKind(nnkIdentDefs)
for j in 0 .. len(param) - 3:
call.add param[j]
meth[6] = nnkStmtList.newTree(call)
newMethods.add(meth)
result = newStmtList()
result.add(vtableTypeDef)
result.add quote do:
type `name` = object
objet : pointer
vtable: ptr `vtableIdent`
for meth in newMethods:
result.add meth
#echo result.repr
macro implementInterface*(interfaceName, implementationCandidateName:
typed) : untyped =
let
vtableSymbol = interfaceName.symbol.getImpl[2][2][1][1][0]
vtableRecordList = vtableSymbol.symbol.getImpl[2][2]
let vtableValueSymbol = newIdentNode($implementationCandidateName.symbol
& "Vtable")
let
objectConstructor = nnkObjConstr.newTree(vtableSymbol)
vtableValueDecalaration =
nnkLetSection.newTree(
nnkIdentDefs.newTree(
vtableValueSymbol,
newEmptyNode(),
objectConstructor
)
)
for identDefs in vtableRecordList:
let
methodName = identDefs[0]
params = identDefs[1][0]
lambdaBody = quote do:
cast[ptr `implementationCandidateName`](this).`methodName`()
call = lambdaBody[0]
for i in 2 ..< len(params):
let param = params[i]
param.expectKind(nnkIdentDefs)
for j in 0 .. len(param) - 3:
call.add param[j]
# leave out () when not needed
if call.len == 1:
lambdaBody[0] = call[0]
methodName.expectKind nnkIdent
objectConstructor.add nnkExprColonExpr.newTree(
methodName,
nnkLambda.newTree(
newEmptyNode(),newEmptyNode(),newEmptyNode(),
params.copy,
newEmptyNode(),newEmptyNode(),
lambdaBody
)
)
result = newStmtList()
result.add vtableValueDecalaration
let castIdent = newIdentNode($interfaceName.symbol & "_InterfaceCast")
result.add quote do:
proc `castIdent`(this: ptr `implementationCandidateName`) :
`interfaceName` = `interfaceName`(
objet : this,
vtable : `vtableValueSymbol`.unsafeAddr
)
# echo result.repr
conclusion: Nim turns out to be a very powerful programming language.