How to ensure that a function definition matches the prototype in a C header file

If I write a library with a C API but implemented in Zig, with a handwritten header file (necessary with -femit-h not working, but it might be preferable anyways to have a user-friendly header with comments etc. instead of something automatically generated), is there a way to check that the implementation in Zig matches the function prototypes of the header file?

If I implemented the library in C, I would simply #include the header file, and the compiler produce errors if something doesn’t match.

But in Zig, for example if the header file mathtest.h is

double add(double a, double b);

and the zig implementation is

const c = @cImport(@cInclude("mathtest.h")); // doesn't do anything

// definition for c.add (but the types are wrong!)
export fn add(a: f32, b: f32) f32 {
    return a + b;
}

this gets compiled to a library and linked to a C program without errors, and just leads to undefined behavior at runtime.

Just a quick idea without having a compiler at hand.

In zig you should be able to compare the types of add and c.add. Place those comparisons in the first line of your exported functions and emit a @compileError if they do not match.

1 Like

A test like this seems to work too:

test "matches header" {
    try @import("std").testing.expectEqual(@TypeOf(c.add), @TypeOf(add));
}

This indeed finds the mismatch (“expected type fn (f64, f64) callconv(.c) f64, found type fn (f32, f32) callconv(.c) f32”).

I would have to check every function though. Maybe it is possible to loop through all functions? But @typeInfo(@This()).@"struct".decls contains only the names of pub functions, and no types.

3 Likes

You can use @field to access a declaration by name.

const decls = @typeInfo(T).@"struct".decls;
inline for (decls) |decl| {
    if (std.meta.hasFn(T, decl.name) {
        const fld = @field(T, decl.name);
        // @TypeOf(fld) works here
    }
}
1 Like

Declare the function as public in addition to using export. That allows you to loop thru the functions:

const std = @import("std");

pub export fn add(a: f32, b: f32) f32 {
    return a + b;
}

test {
    inline for (comptime std.meta.declarations(@This())) |decl| {
        const NewFn = @TypeOf(@field(@This(), decl.name));
        @compileLog(NewFn);
        // ...
    }
}
4 Likes