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:
- pointer to the private data for the type it is implemented;
- pointer to
supermethod.
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 structure:
PwInterface_<interface_name> - method structure:
PwMethod_<interface_name>_<method_name> - function type:
PwFunc_<interface_name>_<method_name>
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: calls super methodpw_super_call: call different super method of the same interface implementationpw_this_call: call method of the same interface implementation
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:
- destroy existing value the
resultpoints to (that's whyPwValuemust be always initialized when declared). - set type_id field in the
resultstructure
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);