Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implementing basic vtable in C

Tags:

I'm trying to emulate the most basic (toy) case of a vtable in C. Here is a basic example:

typedef struct Person {
    int id;
    char *name;
} Person;

And let's say we add in one method (i.e., function pointer):

typedef struct Person {
    int id;
    char *name;
    void (*print_name)(Person);
} Person;

And now we'll initialize it and fill in the pieces with this (let’s ignore memory leaks):

#include <stdio.h>
#include <stdlib.h>

typedef struct Person Person;
typedef struct Person {
    int id;
    char *name;
    void (*print)(Person *self);
} Person;

void print_name(Person *person) {
    printf("Hello %s\n", person->name);
}
Person *init_person(void) {
    Person *person = malloc(sizeof(Person));
    person->print = print_name;
}
int main(void) {
    Person *p = init_person();
    p->name = "Greg";
    p->print(p);
    return 0;
}

Running code here.

If I were to factor out the functions from the Person and put it in a Person_VTable, such as the following:

typedef struct Person {
    int id;
    char *name;
    Person_VTable *vtable;
} Person;

typedef struct Person_VTable {
    ???
} Person_VTable;

What would be the proper way to (1) create the vtable, and (2) initialize the Person object with the new vtable? Note, I know this is an entirely trivial example and it can be done in better ways, but I'm seeing how it can be done with an external 'vtable' to the main object I'm working work.

Also, does this also mean if I have a vtable, instead of just having the one 'self' to reference the object it's coming from when in the struct itself, such as:

void (*print)(Person *self);

I need to have two indirections, so I know both the object and the vtable location? Something like:

void (*print)(Person *self_obj, Person_VTable *self_vt);

If so, that's a lot of overhead!

like image 915
David542 Avatar asked Feb 04 '21 06:02

David542


1 Answers

A basic vtable is nothing more than an ordinary struct containing function pointers, which can be shared between object instances. There are two basic ways one can implement them. One is to make the vtable pointer an ordinary struct member (this is how it works in C++ under the hood):

#include <stdio.h>
#include <stdlib.h>

typedef struct Person Person;
typedef struct Person_VTable Person_VTable;

struct Person {
    int id;
    char *name;
    const Person_VTable *vtable;
};

struct Person_VTable {
    void (*print)(Person *self);
};

void print_name(Person *person) {
    printf("Hello %s\n", person->name);
}

static const Person_VTable vtable_Person = {
    .print = print_name
};

Person *init_person(void) {
    Person *person = malloc(sizeof(Person));
    person->vtable = &vtable_Person;
    return person;
}

int main(void) {
    Person *p = init_person();
    p->name = "Greg";
    p->vtable->print(p);
    return 0;
}

Another is to use fat pointers (this is how it’s implemented in Rust):

#include <stdio.h>
#include <stdlib.h>

typedef struct Person Person;
typedef struct Person_VTable Person_VTable;

typedef struct Person_Ptr {
    Person *self;
    const Person_VTable *vtable;
} Person_Ptr;

struct Person {
    int id;
    char *name;
    const Person_VTable *vtable;
};

struct Person_VTable {
    void (*print)(Person_Ptr self);
};

void print_name(Person_Ptr person) {
    printf("Hello %s\n", person.self->name);
}

static const Person_VTable vtable_Person = {
    .print = print_name
};

Person_Ptr init_person(void) {
    Person_Ptr person;
    person.self = malloc(sizeof(Person));
    person.vtable = &vtable_Person;
    return person;
}

int main(void) {
    Person_Ptr p = init_person();
    p.self->name = "Greg";
    p.vtable->print(p);
    return 0;
}

In C, the preferred way is the former, but that’s mostly for syntax reasons: passing structs between functions by value doesn’t have a widely-agreed-upon ABI, while passing two separate pointers is rather unwieldy syntactically. The other method is useful when attaching a vtable to an object whose memory layout is not under your control.

In essence, the only advantages of vtables over ordinary function pointer members is that they conserve memory (each instance of the struct only needs to carry one vtable pointer) and protect against memory corruption (the vtables themselves can reside in read-only memory).

like image 78
user3840170 Avatar answered Nov 08 '22 16:11

user3840170