#"null function or function signature mismatch" in Emscripten builds

1 messages ยท Page 1 of 1 (latest)

trail ferry
#

I am using a payload capture of the optional function here before I invoke it. I don't know how it could be happening (Behavior.renderUpdate method attached)

#

Worth mentioning this does not happen on the PC builds

#

Could it be the dangers of @ptrCast at play?

latent dirge
#

shouldnt renderUpdate take an *anyopaque as its first arg and use ptrcast to get a *Behavior?
like this:

pub fn renderUpdate(ptr: *anyopaque, object: *GameObject) void {
  const self: *Behavior = @ptrCast(ptr);
}
#

and yes ptrCasting function pointers is generally a bad idea

#

*anyopaque is a single pointer with an erased type, and you're casting the function into taking a Behavior (which is afaict an interface object) which is pretty far off from being a pointer

trail ferry
#

i have a bit of an interface approach

latent dirge
#

yeah, like writeAll shouldnt you be taking *anyopaque in your interface implementation and casting it to its actual type

real knot
#

yeah this came up when we were talking with mike and some folks and showing you this interface thing

trail ferry
#

I was suggested to move the logic to save an extra line of boilerplate at the first line of each method

real knot
#

it's not technically correct to do it the way you have, but it will probably work for native builds

#

it probably won't work here

trail ferry
#

I dont understand what the difference is though

#

Whys it save to do it on the first line of the method but not when setting the fn ptr

real knot
#

the simple thing to follow is, don't ever @ptrCast a function

#

you can @ptrCast a pointer to a struct

#

not to a function

#

(there are nuances to both, but this is a simplified rule)

trail ferry
#

so i should keep that boilerplate at the front of each method is what ur saying, and ditch the ptrcasts

#

even for more verbosity it would ensure working on web platform too

latent dirge
#

you really shouldnt be doing it on any platform

weak tree
#

boooo wasm

trail ferry
#

;-;

trail ferry
latent dirge
#

youre giving it a function pointer that says it takes two pointers, but the function pointer actually takes a struct and a pointer

trail ferry
#

wait nvm, there it's ptrCasting the behaviour struct

#

not the function

#

I understand the issue and how to fix it but I was hoping there would be a way to do it that didnt involve me having to do that boilerplate line at the front of each fn, I guess not

#

to restablish the type

latent dirge
#

theres like, a way to do it without the boilerplate but it involves a bit of work in Behavior

trail ferry
#

eh:/ forget it

latent dirge
#

i can write an example

#

its not that bad

#
const Interface = struct {
    ptr: *anyopaque,
    vtable: *const Vtable,

    const Vtable = struct {
        func: *const fn (*anyopaque) void,
    };

    fn generate(comptime T: type) Vtable {
        return .{
            .func = struct {
                fn f(ptr: *anyopaque) void {
                    T.func(@ptrCast(ptr));
                }
            }.f,
        };
    }
};

const Implementation = struct {
    const myVtable = Interface.generate(Implementation);

    pub fn func(self: *Implementation) void {
        _ = self;
        std.debug.print("hello\n", .{});
    }

    pub fn asInterface(self: *Implementation) Interface {
        return .{ .ptr = self, .vtable = &myVtable };
    }
};

pub fn main() !void {
    var i: Implementation = .{};
    const interface = i.asInterface();

    interface.vtable.func(interface.ptr);
}
#

generate would be the function that wraps all the functions you need to do the ptrCast for you

#

so you trade having to @ptrCast at the start of every implenetation to having to write a wrapper for each vtable function

trail ferry
#

but all that can be hidden away

#

sounds like a good tradeoff

#

the idea is to have as least duplicated code as possible in behaviour files

latent dirge
#

yeah this would get rid of having to define a Behavior.VTable in each of your implementations too

#

oh and another tradeoff would be being "forced" to name your implementation functions a specific thing, but you could just pass each one in to generate to avoid that

trail ferry
#

@here I'm so dumb

#

It was because one of my functions signatures was incorrect

weak tree
#

HA

trail ferry
#

I forgot a parameter and because of the ptrCast of the function

#

It screwed me up...

#

...

#

๐Ÿ˜ญ

weak tree
#

i feel so justified rn

trail ferry
#

Still, maybe a sign I should move away from this design...

real knot
#

it was still ultimately because of the fn ptrcast

#

๐Ÿ˜›

weak tree
#

i mean, sortaaaa

#

but that means instead of generating a wrapper, you could write a comptime signature checker

#

a userland fnPtrCast

trail ferry
#

true

real knot
#

(or cast the struct pointer in the generated code because it's easier to write it so it's always correct by construction)

weak tree
#

but then you get goofy ass callstacks

real knot
#

like the frame with just the cast gets left in?

#

my bigger question would be whether ptrcasting the function defeats devirtualizing optimizations more often

#

because I think it would but I haven't looked

weak tree
#

probably would, though would a framework-y virtual dispath like that even get devirtualized much in the first place?

real knot
#

def not with a bunch of them in a heterogeneous collection, but I could see some other cases where it would

trail ferry
#

So whats the best solution?

#

Of the proposed potential ones in your expert opinions?

weak tree
#

I'm biased ๐Ÿ˜›
but I think the raw ptrcast is probably not it, you'll want something to catch mismatches like this, so either a wrapper generator, or a userland fnPtrCast that knows how to check for "compatible" fn signatures
or just do the manual casting

real knot
#

oh look

#

at least in this silly example

#

it does not inhibit devirtualizing

#
#

wild

weak tree
#

hell yeah

weak tree
#

ok I worked from your thing and made a thing which works but I think really speaks to the value a @fnPtrCast builtin would provide: https://godbolt.org/z/bPdaGK684
and I probably got the const variance logic backwards

[EDIT]: better one https://godbolt.org/z/dq1PG6e5K

latent dirge
buoyant solstice
#

My solution would be to not use dynamic dispatch at all ๐Ÿซ 

weak tree
#

true that less dynamic dispatch is usually simpler
just need to be judicious about it

buoyant solstice
#

considering this is a game, and all the behaviours etc are known before hand. Dynamic dispatch doesn't make much sense, unless you really have to squeeze on binary size