Interfaces and Any Type
Working with the type of any
at runtime.
The any
type is recommended for writing code that is polymorphic at runtime where macros are not appropriate.
It can be thought of as a typed void*
.
An any
can be created by assigning any pointer to it. You can then query the any
type for the typeid of
the enclosed type (the type the pointer points to) using the type
field.
This allows switching over the typeid, either using a normal switch:
Or the special any
-version of the switch:
Sometimes one needs to manually construct an any-pointer, which
is typically done using the any_make
function: any_make(ptr, type)
will create an any
pointing to ptr
and with typeid type
.
Since the runtime typeid
is available, we can query for any runtime typeid
property available
at runtime, for example the size, e.g. my_any.type.sizeof
. This allows us to do a lot of work
on with the enclosed data without knowing the details of its type.
For example, this would make a copy of the data and place it in the variable any_copy
:
Variable argument functions with implicit any
Regular typed varargs are of a single type, e.g. fn void abc(int x, double... args)
.
In order to take variable functions that are of multiple types, any
may be used.
There are two variants:
Explicit any
vararg functions
This type of function has a format like fn void vaargfn(int x, any... args)
. Because only
pointers may be passed to an any
, the arguments must explicitly be pointers (e.g. vaargfn(2, &b, &&3.0)
).
While explicit, this may be somewhat less user-friendly than implicit vararg functions:
Implicit any
vararg functions
The implicit any
vararg function has instead a format like fn void vaanyfn(int x, args...)
.
Calling this function will implicitly cause taking the pointer of the values (so for
example in the call vaanyfn(2, b, 3.0)
, what is actually passed are &b
and &&3.0
).
Because this passes values implicitly by reference, care must be taken not to mutate any values passed in this manner. Doing so would very likely break user expectations.
Interfaces
Most statically typed object-oriented languages implements extensibility using vtables. In C, and by extension C3, this is possible to emulate by passing around structs containing list of function pointers in addition to the data.
While this is efficient and often the best solution, but it puts certain assumptions on the code and makes interfaces more challenging to evolve over time.
As an alternative there are languages (such as Objective-C) which instead use message passing to dynamically typed objects, where the availability of a certain functionality may be queried at runtime.
C3 provides this latter functionality over the any
type using interfaces.
Defining an interface
The first step is to define an interface:
While myname
will behave as a method, we declare it without type. Note here that unlike normal methods we leave
out the first “self”, argument.
Implementing the interface
To declare that a type implements an interface, add it after the type name:
If a type declares an interface but does not implement its methods, then that is compile time error.
A type may implement multiple interfaces, by placing them all inside of ()
e.g. struct Foo (VeryOptional, MyName) { ... }
A limitation is that only user-defined types may declare they are implementing interfaces. To make existing types implement interfaces is possible but does not provide compile time checks.
One of the interfaces available in the standard library is Printable, which contains to_format
and to_new_string
.
If we implemented it for our struct above it might look like this:
“@dynamic” methods
A method must be declared @dynamic
to implement an interface, but a method may also be declared @dynamic
without
the type declaring it implements a particular interface. For example, this allows us to write:
@dynamic
methods have their reference retained in the runtime code and can also be searched for at runtime and invoked
from the any
type.
Referring to an interface by pointer
An interface e.g. MyName
is can be cast back and forth to any
, but only types which
implement the interface completely may implicitly be cast to the interface.
So for example:
Calling dynamic methods
Methods implementing interfaces are like normal methods, and if called directly, they are just normal function calls. The difference is that they may be invoked through the interface:
If we have an optional method we should first check that it is implemented:
We first query if the method exists on the value. If it does we actually run it.
Here is another example, showing how the correct function will be called depending on type, checking
for methods on an any
:
Reflection invocation
This functionality is not yet implemented and may see syntax changes
It is possible to retrieve any @dynamic
function by name and invoke it:
This feature allows methods to be linked up at runtime.