💾 Archived View for koyu.space › aartaka › public › oop-c.gmi captured on 2024-05-10 at 10:45:55. Gemini links have been rewritten to link to archived content

View Raw

More Information

➡️ Next capture (2024-05-26)

-=-=-=-=-=-=-

đź“Ž Object-Oriented C: A Primer

By Artyom Bologov

"A sea shore landscape with fishermen butchering the fish they caught only to get smaller fish out of them."

C is a marvelous language in how it can be small, efficient,

pretty,

and ugly,

all at the same time.

But what it lacks (driving modern developers to despair) is Object-Oriented Programming (OOP) facilities.

Yes, there's C++, but one doesn't talk about C++ in good social circles.

Especially when it's possible to simulate classes, objects, and methods in C.

Yes, it'll look slightly irregular, but bear with me.

This post is structured around Object-Oriented Programming concepts.

Starting from the simplest ones and slowly increasing the difficulty level.

Be prepared: OOP is quite a fuzzy set of ideas.

I'll exploit this fuzziness as much as possible—to

stay within what people call OOP while not parting with C and its blessed ways.

As a spoiler: the resulting system will be a Generic-based Single Inheritance one.

Here's a problem domain we're going to model: animals.

There are animals (yes, a classic OOP example).

Many families and species of animals.

I have a lovely cat named Kalyam, so I'm mostly interested in Feline.

"A diagram with relations of animal species. Feline and Canine are Carnivores, while Birds are mere Animals."

I'm going to model just that part of biological hierarchy.

Hopefully, we'll have enough material to crash-test the approach I suggest.

Encapsulation (#encapsulation)

This one is easy.

In its simplest definition, encapsulation is putting things into their buckets (called classes).

Encapsulation might also mean hiding data inside classes, but see

visibility section (#visibility)

for that instead.

Encapsulation might also mean belonging of behavior (methods) inside classes.

But that's debatable: some languages have a generic-oriented OOP where methods are freestanding entities.

If anything, methods belong to their generic functions there.

That's the approach I'll use.

C (since C11) has generic dispatch, so

why not use the feature that's already there?

Putting the generics talk aside for a second, let's encapsulate some data, shall we?

struct animal {
        char *name;
        char *species;
};

That's it, we've got our parent class ready.

The data is contained within, so we've got encapsulation.

What we don't yet have are animal classes, orders, genera, species, etc.

So let's create an extinct animal that can forgive me my poor biology knowledge:

struct animal oldie = {.name = "Oldie", .species = "Miacid"};
printf("This really old animal is %s of %s specie\n", oldie.name, oldie.species);

In case you don't like this

struct talk, you can always define a macro and a type alias (capitalized, to please the Java crowd):

 #define class struct
typedef class animal Animal;

That's all there is to encapsulation, really.

Inheritance (#inheritance)

With encapsulation out of the way, enter inheritance.

A way for classes to depend on each other and share behaviors/data.

A trick I learned

(from the "Good Taste" series of posts)

just a few days ago: embedding structures inside each other.

Putting one structure as the first member of another,

you're making the outer structure castable to the inner one,

sharing the behavior between the two:

typedef class carnivoire {
        Animal parent;
} Carnivoire;

Now we can cast any animal to (Animal *) and invoke Animal methods:

Carnivoire sabre_tooth = {{.name = "Diego", .species = "Dinictis"}};
eats((Animal *)&sabre_tooth);

No, wait our animals don't know how to eat yet!

Let's teach them—with polymorphism!

Polymorphism (#polymorphism)

There's a default behavior to animals: they eat (duh).

That's why the diagram above includes the

eats() method: any animal class eats something.

However, there are all sorts of animals.

Some eat plants.

Some eat fungus.

Some eat other animals (huh, a recursion?)

Knowing that the creature is an animal, it's hard to tell what they eat.

Here's how we express it with code:

void animal_eats (Animal *self)
{
        printf("%s eats ???\n", self->name);
}

 #define eats(animal)
_Generic((animal),
         default: animal_eats)
((animal))

For now, our eats() macro/generic only has one default method: animal_eat.

But you can already see how one can extend it with just another line of type+method.

Let's actually do that:

void carnivoire_eats (Carnivoire *self)
{
        printf("%s eats meat (a shame—it involves killing other animals)\n", self->parent.name);
}

 #define eats(animal)
_Generic((animal),
         Carnivoire *: carnivoire_eats,
         default: animal_eats)
((__VA_ARGS__))

Only one more line in the generic, and we have carnivoire-specific behavior!

That's what polymorphism's promise is: specifying behavior given a type.

eats(&sabre_tooth); // Diego eats meat...

Visibility (#visibility)

Most OOP systems have private/public/protected differentiation.

I can easily cast it aside based on the fact that e.g. Python doesn't have visibility as a concept.

But I'll try to implement it anyway.

The trick is treating structures as opaque data.

I mean, the user of the code doesn't have to know the data layout of the structure.

They have to use it as a raw pointer, relying on extern-ed functions anyway.

This is exploited by many codebases.

They tend to hide the pointer to the "private" version of the structure, nested inside the "public" one.

WebKitGTK does this:

class WebKit2.WebViewBase : Gtk.Container
  implements Atk.ImplementorIface, Gtk.Buildable {
  priv: WebKitWebViewBasePrivate*
}

Relying on this tradition, we can say that structures are private by default.

What's public is their getters and setters.

So why not define some getters and setters for our structures?

char *animal_get_name (Animal *self)
{
        return self->name;
}
void animal_set_name (Animal *self, char *name)
{
        self->name = name;
}

This is pretty boring, so let's define a new class with private fields and methods:

 #define private
typedef class feline {
        Carnivoire parent;
        private bool claws_out;
} Feline;

// Don't have to define name getter/setter: animal has it already.
bool feline_get_claws_out (Feline *self)
{
        return self->claws_out;
}
void feline_protract_claws (Feline *self)
{
        self->claws_out = true;
}
void feline_retract_claws (Feline *self)
{
        self->claws_out = false;
}

Notice that we don't have a setter for

claws_out—retract/protract methods handle modification.

An important OOP technique of hiding the actual data behind the behavior.

Using the OOP system (#using-oop)

The code so far was pretty simplistic, and there wasn't much OOP.

This section and example will finally put the system to the test.

Let's define cats (I've been waiting for this!) and their behaviors:

typedef class cat {
        Feline parent;
} Cat;

void cat_purr (Cat *self)
{
        printf("%s purrs...\n", animal_get_name(self));
        feline_retract_claws(self);
}
void cat_eats (Cat *self)
{
        printf("%s eats mice\n", animal_get_name(self));
}

 #define eats(animal)
_Generic((animal),
         Carnivoire *: carnivoire_eats,
         Cat *: cat_eats,
         default: animal_eats)
((animal))

Testing this system yields:

// My little sweet boy
Cat Kalyam = {{{{.name = "Kalyam", .species = "Felis catus"}}, .claws_out = true}};

printf("%s's claws are %stracted\n",
       animal_get_name(&Kalyam),
       (feline_get_claws_out(&Kalyam) ? "pro" : "re"));
// Kalyam's claws are protracted

eats(&Kalyam);
// Kalyam eats mice

cat_purr(&Kalyam);
printf("%s's claws are %stracted\n",
       animal_get_name(&Kalyam),
       (feline_get_claws_out(&Kalyam) ? "pro" : "re"));
// Kalyam's claws are retracted

These nested curly brackets are not looking right, begging for constructor methods.

But this post is too long already, so let's leave it for later.

What's important: Encapsulation, Inheritance, Polymorphism, and Visibility are there.

C can do OOP.

And it's not that hard really—what this post covers is quite simple and easy to scale.

You can look at the final code (compiles with GCC and Clang, even if with heaps of warnings)

in oop-c-primer-original.c.

And the cleaned-up

(and slightly extended with bird-specific details

if you wanted to see a more involved inheritance hierarchy) code

in oop-c-primer-cleanup.c.

I hope that you're convinced OOP is possible in C, for better or worse.

Thanks for accompanying me on this journey!

Acknowledgements (#acknowledgements)

I owe a thanks to

Post-Modern C style guile

for making me even consider a possibility of OOP in C.

The Extendible _Generic post from CC

for all too late a realization of a better approach.

They did OOP better, let's agree on that.

This website is

Designed to Last

and generated with the help of

C preprocessor.

You can view page sources by appending .h to the page URL.

Copyright 2022-2024 Artyom Bologov (aartaka).

Any and all opinions listed here are my own and not representative of my employers; future, past and present.

Back to home page

About me

My projects