LogoChemical Docs
Ctrl+K

Interfaces & Dynamic Dispatch

Interfaces define a set of methods that a type must implement. Chemical supports two forms of interface usage: Static and Dynamic.

Interface Definition

Any struct or variant can implement an interface by defining the required methods.

interface Speaker {

    func say_hi(&self)

}



struct Robot {

    var id : int

}



// Implementing the interface

impl Speaker for Robot {

    func say_hi(&self) {

        printf("Beep boop, I am Robot %d\n", self.id)

    }

}

Implementation Patterns

When you implement an interface using impl or by adding it to inheritance list, You must implement all its methods at once, otherwise compiler would throw the error for missing methods.

Using impl Blocks

The most common way to implement an interface:

impl Speaker for Robot {

    func say_hi(&self) {

        printf("Hello from Robot %d\n", self.id)

    }

}

Inline Implementation

You can also implement interfaces directly when defining a struct using the : syntax:

struct Dog : Speaker {

    var name : *char

    

    @override

    func say_hi(&self) {

        printf("Woof! I'm %s\n", name)

    }

}

Static Interfaces (@static)

Static interfaces are resolved at compile time. They behave like C function declarations. Static interfaces can only be implemented once. Compiler throws an error if it finds multiple implementations for a static interface.

When to Use Static Interfaces

@static

interface Adder {

    func add(&self, other : int) : int

}



struct Number : Adder {

    var value : int

    

    @override

    func add(&self, other : int) : int {

        return value + other

    }

}

Static Interface Usage

Static interface methods are called directly without virtual dispatch:

func do_add(obj : &Adder, val : int) : int {

    return obj.add(val)  // Direct call, no vtable lookup

}

Extension Functions on Static Interfaces

You can add extension functions to static interfaces, but not on normal interfaces:

@static

interface Summer {

    func sum(&self) : int

}



// Extension function accessible by any type implementing Summer

func (s : &Summer) triple_sum() : int {

    return s.sum() * 3

}

Dynamic Dispatch (dyn)

When you need to store different types that implement the same interface in a single list or pass them around without knowing their exact type at compile time, you use Dynamic Dispatch.

Creating Dynamic Objects

Use the dyn keyword to create a fat pointer:

var r = Robot { id : 1 }

var s : dyn Speaker = dyn<Speaker>(r)



s.say_hi()  // Dynamic dispatch via vtable

Fat Pointer Layout

A dyn Interface type is a fat pointer consisting of:

  1. A pointer to the object instance
  2. A pointer to the vtable (the interface implementation)

This is essentially 16 bytes on 64-bit systems (two pointers).

Using Dynamic Objects in Functions

func make_speak(speaker : dyn Speaker) {

    speaker.say_hi()

}



var robot = Robot { id : 42 }

make_speak(dyn<Speaker>(robot))

Dynamic Objects as Return Values

dynamic objects are just references and become invalid as soon as the original object dies so should be used carefully.

func create_speaker(robot_mode : bool) : dyn Speaker {

    if (robot_mode) {

        return dyn<Speaker>(Robot { id : 1 })

    } else {

        return dyn<Speaker>(Dog { name : "Buddy" })

    }

}

Dynamic Objects in Structs

It's basically storing a reference, Be careful to not create a dangling reference.

struct SpeakerContainer {

    var speaker : dyn Speaker

}



var container = SpeakerContainer {

    speaker : dyn<Speaker>(Robot { id : 10 })

}

container.speaker.say_hi()

Safety & Lifetimes

CAUTION
Dynamic interface pointers (dyn) are powerful but require care. If the underlying object is deallocated or goes out of scope while a dyn pointer still exists, calling methods on it will result in a crash or undefined behavior.
var s : dyn Speaker

{

    var r = Robot { id : 99 }

    s = dyn<Speaker>(r)

}

// s.say_hi() // DANGEROUS! r has been destroyed.

WARNING
The Chemical compiler is currently in pre-alpha. There is no lifetime checking to prevent dangling dyn references. You must manually ensure object lifetime exceeds the lifetime of any dyn pointers.

Safe Pattern: Allocate on the heap to extend lifetime:

unsafe {

    var robot_ptr = new Robot { id : 100 }

    var speaker : dyn Speaker = dyn<Speaker>(*robot_ptr)

    

    // Use speaker...

    speaker.say_hi()

    

    // Clean up when done

    destruct robot_ptr

}

Multiple Interface Implementation

A single struct can implement as many interfaces as needed:

interface Printable {

    func print(&self)

}



interface Serializable {

    func serialize(&self) : *char

}



struct MultiTool {

    var data : int

}



impl Speaker for MultiTool {

    func say_hi(&self) {

        printf("Hi from MultiTool\n")

    }

}



impl Printable for MultiTool {

    func print(&self) {

        printf("MultiTool: %d\n", data)

    }

}

Interface with Inheritance

When a struct inherits from another struct, it can implement different interfaces at each level:

struct Animal {

    var age : int

}



impl Speaker for Animal {

    func say_hi(&self) {

        printf("Animal sound\n")

    }

}



struct Dog : Animal {

    var name : *char

}



// Dog inherits Speaker implementation from Animal

// But can also implement additional interfaces

impl Printable for Dog {

    func print(&self) {

        printf("Dog: %s, age %d\n", name, age)

    }

}