Dynamic code

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.

An any value 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:

switch (my_any.typeid)
{
    case Foo.typeid:
        ...
    case Bar.typeid:
        ...
}

Or the special any-version of the switch:

switch (my_any)
{
    case Foo:
        // my_any can be used as if it was Foo* here
    case Bar:
        // my_any can be used as if it was Bar* here
}

Sometimes one needs to manually construct an any, and it can be done similar to creating any struct type: any { 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.typeid.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:

void* data = malloc(a.type.sizeof);
mem::copy(data, a.ptr, a.type.sizeof);
any any_copy = { data, a.type };

Dynamic calls

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 @interface and @dynamic annotations.

Defining an interface

The first step is to define an interface, this is done by defining an any method without a body annotated @interface:

fn String any.myname(void*) @interface;

Note how void* rather than any is the first parameter. Other than this, it is like any other method declaration.

Implementing the interface

After the interface is added, we can create methods that implement this interface.

struct Bob { int x; }
fn String Bob.myname(Bob*) @dynamic { return "I am Bob!"; }

fn String int.myname(int*) @dynamic { return "I am int!"; }

One of the interfaces available in the standard library is to_string. If we implemented it for our struct above it might look like this:

fn String Bob.to_string(Bob* bob, Allocator* using) @dynamic
{
    return string::printf("Bob(%d)", bob.x, .using = using);
}

Calling dynamic methods

@dynamic methods are just like normal methods. If called directly, they are just normal function calls. The difference is that they may be invoked through any:

An example helps illustrate the typical use:

fn void whoareyou(any a)
{
    // Query if the function exists
    if (!&a.myname)
    {
        io::printn("I don't know who I am.");
        return;
    }
    // Dynamically call the function
    io::printn(a.myname());
}

We first query if the method exists on the value wrapped by any. If it doesn't then we print "I don't know who I am." otherwise we the value's myname() method and print it.

We could use it like this:

fn void main()
{
    int i;
    double d;
    Bob bob;

    any a = &i; 
    whoareyou(a); // Prints "I am int!"
    a = &d;
    whoareyou(a); // Prints "I don't know who I am."
    a = &bob;
    whoareyou(a); // Prints "I am Bob!"
}

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.