I've always been a curious person when it comes to computing—someone who thinks, "Okay, I understand how to use this, but how does it actually work?" As part of this mindset, I like to challenge myself: if I had to implement this from scratch, how would I do it? In this article, we'll explore how interfaces work in object-oriented programming using Java and then implement a rudimentary version of an interface in C.
Let’s Dive into Our Example
Our example will be simple: we’ll calculate the price of a vehicle. If it’s a car, the price will be based on the speed it can reach; if it’s a motorcycle, the price will be based on its engine displacement (cc). We’ll start by defining the behavior of a vehicle with an interface:
public class Main {
public interface Vehicle {
Integer price();
}
}
Nothing fancy here—just a method that returns an integer. Now let’s implement the Car
class:
public class Main {
// ...
public static class Car implements Vehicle {
private final Integer speed;
public Car(Integer speed) {
this.speed = speed;
}
@Override
public Integer price() {
return speed * 60;
}
}
}
Classic: a constructor and the implementation of the price
method, multiplying the speed by 60
. Next, we implement the Motorcycle
class:
public class Main {
// ...
public static class Motorcycle implements Vehicle {
private final Integer cc;
public Motorcycle(Integer cc) {
this.cc = cc;
}
@Override
public Integer price() {
return cc * 10;
}
}
}
Practically the same, with the only difference being that now we multiply the engine displacement by 10
. Let’s then implement a method to print the price of a vehicle:
public class Main {
// ...
public static void printVehiclePrice(Vehicle vehicle) {
System.out.println("$" + vehicle.price() + ".00");
}
}
Straightforward. Finally, our main
method:
public class Main {
// ...
public static void main(String[] args) {
Car car = new Car(120);
Motorcycle motorcycle = new Motorcycle(1000);
printVehiclePrice(car);
printVehiclePrice(motorcycle);
}
}
$ java Main.java
$7200.00
$10000.00
This is the model we want to achieve, but now from scratch, in C.
How Are We Going to Solve This?
When I think about objects, the first thing that comes to mind is a set of data representing a state and methods to manipulate and manage that state. The most direct way to represent a set of data in C is a struct
. For methods, the closest equivalent would be a function that receives the state as an argument. This state would correspond to the this
in a class, for instance. A practical example would be:
typedef struct {
int height_in_cm;
int weight_in_kg;
} Person;
float person_bmi(Person *person) {
float height_in_meters = (float)person->height_in_cm / 100;
float bmi =
(float)person->weight_in_kg / (height_in_meters * height_in_meters);
return bmi;
}
Here, we define the data of a person in the struct Person
and use it to perform a simple calculation. This is one of the closest things we can have to a class in C. Maybe using function pointers inside the struct
is also a good idea? Well, that’s a topic for another article.
Okay, we have a kind of class. Now, how can we define an interface in C? If you think about it, a compiler/interpreter doesn’t use magic to figure out which classes implement an interface. It determines this at compile time and replaces all parts where we use interfaces with the concrete types. In the compiled program, interfaces don’t even exist.
Since the C compiler doesn’t give us this capability, we’ll have to implement this whole mechanism ourselves. We need to know all the types that implement our interface and find a way to use the functions of these implementations.
Implementing an Interface in C
To start, let’s define the skeleton of our rudimentary interface. We’ll create an enum
for the different implementations and declare the signatures of our functions.
#include <stdio.h>
#include <stdlib.h>
typedef enum { VEHICLE_CAR, VEHICLE_MOTORCYCLE } VehicleType;
typedef struct {
VehicleType type;
} Vehicle;
void vehicle_free(Vehicle *vehicle);
int vehicle_price(Vehicle *vehicle);
Here, we define our enum
with the implementations we’ll create later. It may not seem like much, but this is the most important part. Then we declare the vehicle_free
function, which I’ll explain later, and the vehicle_price
function, which we want to implement in our "classes." Now let’s implement the Car
:
// ...
typedef struct {
VehicleType type;
int speed;
} Car;
Car *car_init(int speed) {
Car *car = malloc(sizeof(Car));
car->type = VEHICLE_CAR;
car->speed = speed;
return car;
}
void car_free(Car *car) {
free(car);
}
int car_price(Car *car) {
return car->speed * 60;
}
The car_init
function initializes a new Car
"object" in memory. In Java, this would be done automatically with new
. Here, we must handle it manually. The vehicle_free
function will be used to free the memory allocated for any "object" initialized earlier, using implementations like car_free
. The Motorcycle
implementation is quite similar:
// ...
typedef struct {
VehicleType type;
int cc;
} Motorcycle;
Motorcycle *motorcycle_init(int cc) {
Motorcycle *motorcycle = malloc(sizeof(Motorcycle));
motorcycle->type = VEHICLE_MOTORCYCLE;
motorcycle->cc = cc;
return motorcycle;
}
void motorcycle_free(Motorcycle *motorcycle) {
free(motorcycle);
}
int motorcycle_price(Motorcycle *motorcycle) {
return motorcycle->cc * 10;
}
Pretty much the same—only now we initialize with VEHICLE_MOTORCYCLE
and multiply by 10
. Now let’s implement the function to print the vehicle’s price:
// ...
void print_vehicle_price(Vehicle *vehicle) {
printf("$%d.00\n", vehicle_price(vehicle));
}
So simple… Seeing it like this, it doesn’t even feel like we’re doing all this work. Finally, and most importantly, we need to implement the functions we declared when defining our interface earlier. Fortunately, we don’t need to overthink this implementation. We’ll always have a simple exhaustive switch/case, nothing more.
// ...
void vehicle_free(Vehicle *vehicle) {
switch (vehicle->type) {
case VEHICLE_CAR:
car_free((Car *)vehicle);
break;
case VEHICLE_MOTORCYCLE:
motorcycle_free((Motorcycle *)vehicle);
break;
}
}
int vehicle_price(Vehicle *vehicle) {
switch (vehicle->type) {
case VEHICLE_CAR:
return car_price((Car *)vehicle);
case VEHICLE_MOTORCYCLE:
return motorcycle_price((Motorcycle *)vehicle);
}
}
Now we can use everything we’ve built:
// ...
int main(void) {
Car *car = car_init(120);
Motorcycle *motorcycle = motorcycle_init(1000);
print_vehicle_price((Vehicle *)car);
print_vehicle_price((Vehicle *)motorcycle);
vehicle_free((Vehicle *)car);
vehicle_free((Vehicle *)motorcycle);
return 0;
}
$ gcc -o main main.c
$ ./main
$7200.00
$10000.00
It works! But you might be thinking, "Okay, but what’s the point?"
A Real-World Use Case
One of my favorite types of projects is parsers, ranging from interpreters to simple math expression parsers. When working on these, you’ll often encounter something called an AST (Abstract Syntax Tree). As the name suggests, it’s a tree that represents the syntax you’re processing. For example, a variable declaration like int foo = 10;
is a node in the AST containing three child nodes: one for the type (int
), one for the identifier (foo
), and one for the expression (10
), which itself contains another node for the integer value 10
. See how complex this gets?
When doing this in C, you have to choose between a giant struct
with fields for all possible nodes in the AST or several smaller structs
implementing an abstract definition, each representing a different node—like we did here with our "interface." If you want a simple example, check out this math expression parser, where I implement the latter approach.
Conclusion
Nothing a compiler or interpreter does is magic. Trying to implement something yourself is always an interesting exercise. I hope this was an enjoyable read. Thank you!
Top comments (0)