Interfaces

Interface is a set of functions that has name and id. Actually, only id matters, the name is just for introspection.

A type may implement multiple interfaces. The only mandatory interface is the Basic one.

Different types implement different interfaces, and given that PetWay uses 16 bit for interface id, reserving an array for 65536 interfaces for each type is not a good idea. Instead, PetWay uses the array for built-in interfaces only, and a lookup table for the rest. As long as the number of user-defined interfaces implemented by particular type is usually countable by paws, the performance is quite acceptable.

Given that specific type implements specific interface, the opposite is also true: specific interface implementation is only for specific type. In other words, interface methods work only with specific type. If it's a subtype of Struct, they know only about private data defined for that type, and nothing else. Of course, exceptions are possible, but that's the basic rule.

So, the offset of private data could be known even at compile time, and that was the case in early PetWay versions when types could be defined statically. But with multiple inheritance the offset of private data can vary. It depends on the order of base types and the same type can be at different positions in different subtypes. The same problem arises when we call super method: it depends on the order of base types for particular subclass.

So, each method needs two things that must be super fast:

  1. pointer to the private data for the type it is implemented;
  2. pointer to super method.

PetWay solution is MRO chains and mthis argument in addition to self. While self points to a value which is an instance of the type, mthis points to a structure in MRO chain which contain data offset, pointer to the super method, and introspection data. MRO chains are built for each method for each interface and type.

Interface registration

Interfaces must be registered before creating any type that implements them.

The way it is implemented in PetWay is tricky and somewhat weird. Yet pet does not know hot improve it.

The basic thing the code that builds MRO chains needs to know is how many methods each interface has. But for everything else the best way to define interface is a structure with typed function pointers.

Given that structure with N function pointers maps exactly to array of the same length, we could just say pw_register_interface(sizeof(MyInterface)/sizeof(void(*)())). However, for introspection purposes, PetWay uses the following form which accepts interface name and method names:

PwInterfaceId_Foo = pw_register_interface("Foo", "foo", "bar", nullptr);

This raises the problem how to keep method names in sync with the interface structure. A slight mismatch in the number of methods or in their order would lead to weird runtime errors.

As a solution, pet uses X macros to declare interface methods.

However, X macros have problems when used in lists because C has no such syntax sugar as comma after the last element.

As a workaround pet uses variadic arguments in X macros and VA_OPT to emit comma. Here's an example for Socket interface:

#define PW_SOCKET_INTERFACE_METHODS  \
    X(bind,         1)  \
    X(reuse_addr,   1)  \
    X(listen,       1)  \
    X(is_listening, 1)  \
    X(accept,       1)  \
    X(connect,      1)  \
    X(shutdown,     1)  \
    X(get_socket_error)    // the last member has no extra argument

Now, the interface can be registered. This is done in the same _pw_init_socket function from type-system.md example, right before pw_add_type2:

    if (PwInterfaceId_Socket == 0) {
        _pw_init_interfaces();
#       define X(name, ...) #name __VA_OPT__(,)
        PwInterfaceId_Socket = pw_register_interface("Socket", PW_SOCKET_INTERFACE_METHODS, nullptr);
#       undef X
    }

Interface declaration

pw_add_type and pw_add_type2 require interface structures that contain pointers to functions. Such structures are declared with PW_INTERFACE_BEGIN and PW_INTERFACE_END macros:

PW_INTERFACE_BEGIN(Socket)
#define X(name, ...) PwMethod_Socket_##name name;
    PW_SOCKET_INTERFACE_METHODS
#undef X
PW_INTERFACE_END(Socket)

However, before declaring interface structure, all methods should be defined. In the above example method names start from PwMethod_Socket_ (see X macro).

Method structures are defined with PW_METHOD_BEGIN and PW_METHOD_END. For example,

PW_METHOD_BEGIN(Socket, bind)
    bool (*PwFunc_Socket_bind)(PwMethod_Socket_bind* mthis, PwValuePtr self, PwValuePtr local_addr)
PW_METHOD_END(Socket, bind)

Way too cumbersome, but some forward declaration are defined before function prototype in PW_METHOD_BEGIN and completed in PW_METHOD_END. The function prototype is not shortened for ease of copy-paste to the implementation.

All these macros impose strict naming conventions:

Interface implementation

When interface is declared, its structure can be initialized with function pointers:

static PwInterface_Socket socket_interface = {
#define X(name, ...) .name = { .func = socket_##name } __VA_OPT__(,)
    PW_SOCKET_INTERFACE_METHODS
#undef X
};

socket_interface is the final definition of the interface and as you could see in type-system.md example, its address is passed to pw_add_type2 along with PwInterfaceId_Socket. The X macro initializes the structure with functions defined for each method. Their names start from socket_: socket_bind, socket_listen, etc.

There's another way, when only a few methods need to be implemented in a subclass. Pet uses whichever is shorter:

static PwInterface_MySocket my_socket_interface = {
    .listen = { .func = my_socket_listen },
    .accept = { .func = my_socket_accept }
};

If a subclass need to override a bunch of methods except one or two, X macros can be used with unused functions defined as null:

#define my_socket_connect          nullptr
#define my_socket_get_socket_error nullptr

static PwInterface_MySocket my_socket_interface = {
#define X(name, ...) .name = { .func = my_socket_##name } __VA_OPT__(,)
    PW_SOCKET_INTERFACE_METHODS
#undef X
};

An diligent reader may notice PW_INTERFACE_* macros define quite complex structure that contain an id field, so why not to initialize it along with function pointres and get rid of interface id parameters in pw_add_type2 call?

The answer is simple: interface id is not known at compile time.

Calling interface methods

Methods are called with pw_call wrapper whose first two arguments are interface name and method name. For example,

pw_call(Socket, connect, sock, remote_addr)

Methods have more options that take advantage of mthis:

pw_super is a simple wrapper that expands to:

mthis->super->func(mthis->super, /* args */ )

pw_super_call invokes super for a different method. This is useful to call underlying destructor if an error occured in the constructor after calling super:

if (!pw_super(mthis, result, nullptr)) {
    return false;
}
if (init()) {
    return true;
}
if (!pw_super_call(destroy, mthis, result, nullptr)) { /* no op */ }
return false;

pw_this_call can be useful to call neighbour method of the same interface implementation, but there should be a reason to do that and clear understanding of all implications. As a general rule use pw_call so overloaded methods will be honored.

Basic interface

As it was already mentioned, all types must implement the Basic interface. However, not all methods of this interface are mandatory. For example, destroy and related method decref are optional. The universal destructor pw_destroy does not call them if they are null. clone and deepcopy are optional too. If the type is integral and has no allocated data, the value is simply copied by wrapper functions pw_clone and pw_deepcopy.

Constructor

create method is mandatory for all types. Besides mthis, it accepts two arguments: result and ctor_args.

This method is never created directly. It is called by pw_create or pw_create2 wrappers that do two things:

  1. destroy existing value the result points to (that's why PwValue must be always initialized when declared).
  2. set type_id field in the result structure

So, create method must initialize all remaining bits of PwValue structure. If the type is inherited from Struct, it must call super method in the very beginning so the memory will be allocated.

In case of error the constructor must call destroy method using pw_super_call, set status in the current_task structure (see calling conventions, and return false.

Here's an example of Socket constructor:

static bool socket_create(PwMethod_Basic_create* mthis, PwValuePtr result, PwCtorArgs* ctor_args)
{
    if (!pw_super(mthis, result, ctor_args)) {
        return false;
    }
    _PwSocket* s = get_this_socket(result);
    PwSocketCtorArgs* args = pw_get_ctor_args(PwTypeId_Socket, ctor_args);

    s->sock = socket(args->domain, args->type, args->protocol);
    if (s->sock != -1) {
    
        // init other fields of _PwSocket
        
        return true;
    }
    int _errno = errno;
    if (!pw_super_call(destroy, mthis, result, nullptr)) { /* no op */ }
    pw_set_status(PwErrno(_errno));
    return false;
}

Passing arguments to constructor is a bit cumbersome. The safest way would be using a Map for arguments, but that's not a performant solution. Instead, PetWay uses a chain of structures and lookup by type id. Socket creation looks like this (in real applications use pw_socket wrapper):

PwSocketCtorArgs args = {
    .type_id  = PwTypeId_Socket,
    .domain   = AF_INET,
    .type     = SOCK_STREAM,
};
pw_create2(PwTypeId_Socket, &args, result);