Named Opaque Types
A named opaque type is needed to distinguish two different opaque types, in order to provide some level of type-safety when dealing with opaques. Let's take a look at a small example to showcase this. We take this extern.h file:
#include <stdio.h>
#include <stdlib.h>
typedef void *NamedOpaque;
NamedOpaque create_named_opaque() {
printf("Creating new named opaque\n");
NamedOpaque no = (NamedOpaque)malloc(8);
*(size_t *)no = (size_t)100;
return no;
}
void free_named_opaque(NamedOpaque *value) {
printf("Freeing named opaque %p\n", *value);
free(*value);
*value = NULL;
}
and the .fip/config/fip-c.toml file looks like this:
[c]
headers = ["extern.h"]
sources = ["extern.h"]
command = ["clang", "-c", "-x", "c", "__SOURCES__", "-o", "__OUTPUT__"]
and then the main program looks like this:
use Fip.c
def main():
NamedOpaque no = create_named_opaque();
free_named_opaque(&no);
This program will print these lines to the console:
Creating new named opaque Freeing named opaque 0x38a6d4c0
The auto-generated .fip/generated/c.ft file looks like this:
opaque NamedOpaque;
extern def create_named_opaque() -> NamedOpaque;
extern def free_named_opaque(mut NamedOpaque* value);
The typedef void* NamedOpaque directly resulted in a named opaque definition, opaque NamedOpaque;. Named opaques are incompatible with one another. Let's change the program a bit (the fip-c.toml file stays the same). The new extern.h now looks like:
#include <stdlib.h>
typedef void *GLObject;
typedef void *GLFrame;
GLFrame create_frame() {
GLFrame frame = (GLFrame)malloc(8);
*(size_t *)frame = (size_t)100;
return frame;
}
void free_frame(GLFrame *value) {
free(*value);
*value = NULL;
}
GLObject create_object() {
GLObject frame = (GLObject)malloc(8);
*(size_t *)frame = (size_t)100;
return frame;
}
void free_object(GLObject *value) {
free(*value);
*value = NULL;
}
which results in this new auto-generated c.ft file:
opaque GLObject;
opaque GLFrame;
extern def create_frame() -> GLFrame;
extern def free_frame(mut GLFrame* value);
extern def create_object() -> GLObject;
extern def free_object(mut GLObject* value);
and the main file now looks like this:
use Fip.c
def main():
GLFrame frame = create_frame();
GLFrame object = create_object();
free_frame(&frame);
free_object(&object);
Now, when we try to compile the program we get this compile error:
Parse Error at main.ft:5:22 └─┬┤E0000│ 3 │ def main(): 5 │ » GLFrame object = create_object(); ┌─┴──────────────────────┘ └─ Type mismatch of expression ├─ Expected: GLFrame └─ But got: GLObject
This showcases the advantage we have when using named opaque types instead of raw void* in C or opaque in Flint: The compiler better understands the API and understands now that passing a GLObject typed value where a GLFrame is expected is just wrong! As you can see, even though both GLFrame and GLObject are a void* under the hood, they are fundamentally type-incompatible in Flint, preventing you from passing / storing opaque values to functions which expect something else.
This named opaque system, however, greatly depends on the quality of the C library itself (for auto-bindings). For example when a C function takes a raw void* parameter directly, there isn't much the Flint compiler or FIP can do here, in that case a regular opaque type is emitted for the parameter type (like in the free function for example).