ziglang / zig

General-purpose programming language and toolchain for maintaining robust, optimal, and reusable software.
https://ziglang.org
MIT License
35.02k stars 2.56k forks source link

Function pointer to inline function causes linker error #18446

Open ssmid opened 10 months ago

ssmid commented 10 months ago

Zig Version

0.12.0-dev.1871+e426ae43a

Steps to Reproduce and Observed Behavior

const std = @import("std");

inline fn predict_fun() bool {
    return will_have_fun();
}

fn will_have_fun() bool {
    if (@import("builtin").os.tag == .windows) {
        return false;
    } else {
        return true;
    }
}

pub fn main() !void {
    // the @ptrCast makes sense when using interfaces where *anyopaque is used, see snippet below
    const predict_fun_p: *const fn () bool = @ptrCast(&predict_fun);
    std.debug.print("Will have fun: {}\n", .{predict_fun_p()});
}

This causes a linker error:

error: ld.lld: undefined symbol: test2.predict_fun
    note: referenced by test2.zig:17
    note:               ~/.cache/zig/o/4387975daf2b9165b432ee8243712986/test2.o:(test2.main)
    note: referenced by test2.zig:18
    note:               ~/.cache/zig/o/4387975daf2b9165b432ee8243712986/test2.o:(test2.main)

Use case where this is an issue:

const std = @import("std");

var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();

const FunPredictorInterface = struct {
    obj: *anyopaque,
    predict_fun_fn: *const fn(*const anyopaque) bool,

    fn predict_fun(self: *const @This()) bool {
        return self.predict_fun_fn(self.obj);
    }
};

const FunPredictor3 = struct {
    unneccessary: u32 = 1,

    inline fn predict_fun(self: *const @This()) bool {
        _ = self;
        return will_have_fun();
    }   
};

fn choose_fun_predictor() !FunPredictorInterface {
    const fun_predictor = try allocator.create(FunPredictor3);
    fun_predictor.* = FunPredictor3{};
    return .{
        .obj = @ptrCast(fun_predictor),
        // predict_fun must be cast because interface uses *anyopaque
        // but function pointer to inline function should probably not be allowed
        .predict_fun_fn = @ptrCast(&FunPredictor3.predict_fun),
    };
}

fn will_have_fun() bool {
    if (@import("builtin").os.tag == .windows) {
        return false;
    } else {
        return true;
    }
}

pub fn main() !void {
    const fun_predictor = try choose_fun_predictor();
    std.debug.print("Will have fun: {}\n", .{fun_predictor.predict_fun()});
}

Expected Behavior

~The compiler throws an error when creating a reference (&) to a function that is marked as inline.~ See mlugg's answer below.

Vexu commented 10 months ago

Debug build has an LLVM error:

LLVM Emit Object... error(llvm): failed verification of LLVM module:
Global is external, but doesn't have external or weak linkage!
ptr @a.predict_fun
asoderman commented 10 months ago

I have a rough implementation of the desired behavior. I will be submitting a PR for this soon.

mlugg commented 10 months ago

The listed "expected behavior" here is not correct. Holding a pointer to an inline function is perfectly semantically valid: the bug here is that this pointer, via a @ptrCast, is permitted to become runtime-known. The same bug can be triggered by e.g. casting *type to *anyopaque.

A satisfactory solution to this bug will require introducing the concept of a comptime-only value of a runtime-valid type, and appropriate enforcement.