`Proc` vs. function pointer

Using Proc for function pointers in lib bindings causes type safety issues.

A function pointer is literally just a pointer (to a function :smirking_face:). Proc is a function pointer plus an optional pointer to a closure context. Thus the memory size differs (one word vs two words).

Yet, Crystal represents function pointers in C interop as Proc types. The premise is that Proc in C bindings cannot have a closure context and thus it’s represented as only a function pointer. The compiler implicitly expands and shrinks Proc instances at C ABI interchanges. Passing a Proc instance with closure data to a lib call raises a runtime exception.
So the Proc type effectively comes in two variants with different behaviour. But they look the same to the type system. So it cannot protect from errors.

I suppose this is not a huge problem because I’m not aware of anyone having reported a practical issue with this.
Still, it’s a bit of a weak spot in Crystal’s strong type system.

The compiler makes sure that direct handover to C features (function calls, field assignments) correctly validates and restricts Proc instances to a function pointer.
But outside well-defined interfaces, the Proc duality can lead to memory corruption.

When assigning a Proc value to a pointer, there’s no way to tell which data layout the pointer target expects.
If it points to a value in Crystal, it’s expected to be a two-word Proc, if it points to a value in a lib binding, it’s expected to be a one-word Proc.

The following example demonstrates how assigning a Proc instance to a pointer to a lib field overrides adjacent memory.

lib LibFoo
  struct Foo
    foo : Proc(Nil)
  end
end
 
foo = uninitialized StaticArray(LibFoo::Foo, 2)
 
pointerof(foo.to_unsafe.value.@foo).value = Proc(Nil).new(Pointer(Void).new(0x12345_u64), Pointer(Void).new(0x67890_u64))
 
foo[0] # => LibFoo::Foo(@foo=#<Proc(Nil):0x12345>) # OK
foo[1] # => LibFoo::Foo(@foo=#<Proc(Nil):0x67890>) # The assignment shouldn't touch this memory slot

Of course, C bindings and pointers are inherently unsafe features.
Using them requires extra attention to ensure correct and safe semantics.

But this Proc duality seems like an issue one shouldn’t need to worry about. The Crystal compiler’s type strength usually prevents this kind of error class.

To be honest, I’m not sure if we can do much about this, without a major compatibility break (which might not be worth it).
So this thread meant as a means to determine whether there is any merit in discussing any means of improvement.

This used to be an internal compiler error before LLVM pointers became opaque: Compiler Explorer

Personally I wish there is a separate type for C function pointers, so that pointerof in this case would return e.g. a CProc* rather than a Proc*, while Pointer(CProc)#value=(Proc) would perform the closure pointer check. Such a CProc may also support things like C variadic function semantics and call conventions.

I had a direct issue with this that related to how you can’t perform myarr[0].x = y because the [0]. ==> give you a value instead of a reference, so I’d do something like (myarr.to_unsafe + 0).value.x = y ==> works fine. The issue that relates to this is if x in this case is a Proc, and myarr holds structs, then you’d do (myarr.to_unsafe + 0).value.x = ->myfunc. I’m pretty sure this works right and the closure_data doesn’t corrupt the next 8 bytes after x in memory. My personal problem was when trying to set x to a more vague, raw, function poitner type, or if I x was a union of multiple procs, and I was casting the pointer to one of those type. Example:


lib LibTest
  alias MyProc = Proc(Nil) # Just a type alias
  struct MyStruct
    func : MyProc
  end
end

def mymethod
  puts "Hello !"
end

def myintmethod : Int32
  return 10
end

mystructs = uninitialized StaticArray(LibTest::MyStruct, 2)
# mystructs[0].func = ->mymethod
# mystructs[0].func.call # ==> Returns a memory access error. Above line never set the fun

(mystructs.to_unsafe + 0).value.func = ->mymethod
mystructs[0].func.call # These work fine, prints "Hello !"

(mystructs.to_unsafe + 0).value.func = ->myintmethod
puts mystructs[0].func.call # ==> Prints nothing. Proc forces a nil return

So in order for you to use a func pointer with multiple Proc types, you’d have to do something like this

lib LibTest
  union MyProcs
    n : Proc(Nil)
    i : Proc(Int32)
  end

  struct MyStruct
    func : MyProcs
  end
end

def mymethod
  puts "Hello !"
end

def myintmethod : Int32
  return 10
end

mystructs = uninitialized StaticArray(LibTest::MyStruct, 2)

(mystructs.to_unsafe + 0).value.func.n = ->mymethod
mystructs[0].func.n.call

This makes sense. The issue I was having was when the proc takes a type of void*

lib LibTest
  alias MyProc = Proc(Void*, Nil)

  struct MyStruct
    func : MyProc
  end
end

def mymethod(x : Int32*)
  puts "Hello !"
end

def myintmethod : Int32
  return 10
end

mystructs = uninitialized StaticArray(LibTest::MyStruct, 2)

(mystructs.to_unsafe + 0).value.func = ->mymethod(Int32*) # ==> Error: field 'func' of struct LibTest::MyStruct has type Proc(Pointer(Void), Nil), not Proc(Pointer(Int32), Nil)
(mystructs.to_unsafe + 0).value.func = ->mymethod(Void*) # ==> Error: expected argument #1 to 'mymethod' to be Pointer(Int32), not Pointer(Void)

That’s when you fall into this trap, especially if the array is a $variable inside the lib, and not something on Crystal’s end. In C, a void* can be anything, but in Crystal, it can only be a void*.
Now Crystal will semi-cast void* to and from pointer types such as when passing to a function ans such I believe, but not to and from a proc invoking.
So the way I found out to do it, while still binding to a proc type instead of doing something like having func be a void* and then setting it to (->mymethod).pointer which imo is very bad practice because it ignores the proc type completely (What’s the difference between a function pointer and any pointer in Crystal at that point), is to do something like this

lib LibTest
  alias MyProc = Proc(Void*, Nil)

  struct MyStruct
    func : MyProc
  end
end

def mymethod(x : Int32*)
  puts "Hello !"
end

def myintmethod : Int32
  return 10
end

mystructs = uninitialized StaticArray(LibTest::MyStruct, 2)

((mystructs.to_unsafe + 0).as(UInt8*) + offsetof(LibTest::MyStruct, @func)).as(Proc(Int32*, Nil)*).value = ->mymethod(Int32*)
puts mystructs[0].func.call(Pointer(Void).null)

Now this works, and still respects the proc type. You have to manually page in using offsetof since using pointerof (mystructs.to_unsafe + 0).value.func would error because it will say that .func is a call.

But here in lies the issue. Now that Crystal no longer can assume that this Proc is in a C Lib, where a proc is an 8 byte function pointer and not 8 byte fptr + 8 byte closure data, Crystal will write this supposed 16byte proc pointer’s value, instead of treating it as the 8 byte one.
This makes it corrupt the next 8 bytes after func, which in this case will be the next MyStruct#func in the array. Here’s an example that matches my proc use case almost one to one

lib LibTest
  alias MyProcV = Proc(Nil)
  alias MyProcP1 = Proc(Void*, Nil)
  alias MyProcP2 = Proc(Void*, Nil)


  union MyProcs
    v : MyProcV
    p1 : MyProcP1
    p2 : MyProcP2
  end

  struct MyStruct
    func : MyProcs
  end
end

def safemethod(x : Int32*)
  puts "This is your number #{x.value}"
end

mystructs = uninitialized StaticArray(LibTest::MyStruct, 2)
mynum = 10

((mystructs.to_unsafe + 1).as(UInt8*) + offsetof(LibTest::MyStruct, @func)).as(Proc(Int32*, Nil)*).value = ->safemethod(Int32*)
puts mystructs[1].func.v.pointer # The 8 byte pointer of the second struct in the array

((mystructs.to_unsafe + 0).as(UInt8*) + offsetof(LibTest::MyStruct, @func)).as(Proc(Int32*, Nil)*).value = ->safemethod(Int32*)
puts mystructs[0].func.p1.call(pointerof(mynum).as(Void*))

# Overwritten with the closure data from setting the first struct's proc data 
# in a manner where crystal can't assume that it's a Lib proc
puts mystructs[1].func.v.pointer 

In this, you can see how the proc casting thing corrupts the next 8 bytes with the closure_data. There really is just not a pretty way to do this that I have found, all of them have catches.
Either use void* as the function ptr type with no compiler or runtime proc type enforcement (making sure it’s a function pointer and not just a random ptr),
add 8 pad bytes (int64) in the struct so that the lib side can compensate for the extra 8 bytes the Crystal side sets the function pointer to.
I chose the pad bytes for my project, but if you had a C library that the structs needed to be one to one on, you either add the pad bytes on the C side too, or you just accept that you can’t set a proc that way and you’d probably have to use a void* where whoever is using your C lib bindings would have to ensure they don’t use it wrong.

I know this is probably a lot of nothing but this issue plagued my project that had this issue for days. My dream is like @HertzDevil has said which is that we get a C Proc type, that still has type checking, but is at the level where when we cast something to a CProc*, it won’t corrupt extra bytes when we set it.
That would mean no corruption, and the proc can still confirm we aren’t passing invalid data to it with type checking and what not.
It’s also like 7am when I’m typing this and I’ve not slept but I’m probably gonna forget a lot of this if I don’t yap about it now lol. I hope some of this made sense

Interesting! I didn’t realize this would’ve been a module validation error before.
Perhaps we should consider this a regression on the introduction of opaque pointers then.

I agree on the appeal of a separate type for a function pointer. I’m just afraid it’ll cause a bit of friction…

Regarding variadic function args, shout out to one of the oldest still open issues Unable to declare a C function pointer with varargs · Issue #214 · crystal-lang/crystal · GitHub (I meant to reference that in the original comment, but forgot about it).

I have just made a breaking discovery!
I have used (pointerof(mystruct).as(UInt8*) + offsetof(MyStruct, @var)).as(MyVar*) in order to index vars.
I have just found out I can do this
pointerof(mystruct.@var)

Was this .@ thing common knowledge?
To me, this makes it a lot easier to port C’s & and → operators

Not sure if it’s common knowledge. But I guess the documentation doesn’t help for that.
It should explicitly show that you can chain instance variables (mystruct.@var.@foo.@bar would work as well).

When a C struct contains function pointers, using it from Crystal may require some extra work.

For example, suppose the C side has:

struct Handler {
    void (*foo)(Handler *);
    void (*bar)(Handler *);
};

In Crystal, this can be written as:

lib LibFoo
  struct Handler
    foo : (Pointer(Handler) -> Void)
    bar : (Pointer(Handler) -> Void)
  end
end

However, on the Crystal side, we may want to register a block that has a closure as the callback.

x = 10

handler.foo do
  puts x
end

Because this block captures x, it cannot be stored directly in the C struct as a plain function pointer.

In UIng, this is handled by defining an extended struct. The original handler is placed at the beginning, and additional fields are added to store callbacks wrapped with Box.box.

struct HandlerExtended
  base_handler : LibFoo::Handler
  foo_box : Pointer(Void)
  bar_box : Pointer(Void)
end

The C-side foo and bar fields do not receive the user’s block directly. Instead, they are initialized with fixed trampoline functions that capture nothing.

foo: ->(handler : LibFoo::Handler*) {
  ext = handler.as(HandlerExtended*)

  unless ext.value.foo_box.null?
    callback = Box(Proc(Nil)).unbox(ext.value.foo_box)
    callback.call
  end
}

The actual block registered by the user is boxed and stored in the extended part of the struct.

def foo(&block : ->)
  @foo_box = Box.box(block)
  @extended_handler.foo_box = @foo_box
end

When C calls foo, the trampoline is called first. It reinterprets the Handler* as a HandlerExtended*, retrieves the original Crystal Proc from foo_box, and invokes it.

Because the original Handler is the first member of HandlerExtended, the C side can still treat the pointer as a normal Handler*. The additional foo_box and bar_box fields are not visible to the C side.

In this way, the C side receives only ordinary function pointers, while Crystal closures are stored separately and invoked through trampolines.

This technique does not appear to be explicitly documented in the Crystal reference, but it seems to be a common pattern in FFI code.

(Proofread and translated by ChatGPT)

Yeah, this isn’t explicitly documented anywhere in the Crystal documentation. But I’m not sure it needs to. This is pretty much a general pattern that you can implement in many languages, including Crystal. It’s not a special feature in Crystal, it just uses standard language elements.

Also, I’m not sure what’s the connection to the Proc vs function pointer discussion. Of course, it covers similar topic with function pointer in lib bindings. But the core problem is very different, it’s about working around limitations in the C API, instead of type representation issues.

And just to be clear: This mechanism is highly unsafe. For the cast from Handler* to HandlerExtended* means you must ensure that every Handler* that can reach this code path actually points to a HandlerExtended instance.

Certainly, my comment is a little separate from your main point about the dual nature of Proc and the type system.

When Crystal interoperates with C, a C struct may contain function pointer fields. As you pointed out, if a Proc is stored there directly, a runtime exception can happen when that Proc has closure data and the struct is passed to the C library.

What I meant is that, in Crystal, there are many cases where we want to use closures. Because of this, I suspect that in practice many programs already avoid storing a Proc directly in such a struct from the beginning. They may use the pattern described above, or some other similar pattern.


Extending a struct has some risks, but if all instances are always allocated on the Crystal side, I do not think it is a serious problem. Of course, it becomes dangerous if some instances are allocated by C and others by Crystal.

Passing a Crystal closure to a callback without a user_data argument is difficult. I have not done this myself, but it may require libffi to create a trampoline dynamically. In this case, it is easy to accidentally use a Proc where a CProc should be used.

I think it is useful for Proc and CProc to be different types.

I feel like it’s kinda a double edged sword with now having what is pretty much a redundant and useless type (CProc) when used outside of a lib in Crystal. It’s like if union was allowed outside of lib. Redundancy can be a good thing though! Imo the best version would be a proc that only works in a lib binding, like union, but is a different class or general type from a Proc. I know that’s what’s already been said so I guess this is just me agreeing with it lol. It’d be a change my C-loving-heart would greatly enjoy especially if I continue to bind more C libs over to crystal and encounter another pointer-cast issue with Procs

Recently I have come across an instance of a C function pointer taking another C function pointer as an argument (a COM interface, to be exact). Here is a reduced example:

/* cc -c -o ext.obj ext.c */

typedef void (*Func)(int, int, int, int);
typedef void (*Callback)(Func, int, int, int, int);

typedef struct {
  Callback cb;
} Foo;

void twice(Func func, int x, int y, int z, int w) {
  func(x, y, z, w);
  func(x, y, z, w);
}

static const Foo foo_impl = {&twice};

const Foo *get_foo(void) {
  return &foo_impl;
}
@[Link(ldflags: "#{__DIR__}/ext.o")]
lib Lib
  alias Func = Int32, Int32, Int32, Int32 ->
  alias Callback = Func, Int32, Int32, Int32, Int32 ->

  struct Foo
    cb : Callback
  end

  fun get_foo : Foo*
end

foo = Lib.get_foo
foo.value.cb.call(->(x : Int32, y : Int32, z : Int32, w : Int32) : Nil {
  puts [x, y, z, w].join(' ')
}, 1, 2, 3, 4)

The above prints:

0 1 2 3
0 1 2 3

Not only are the arguments offsetted, but it is possible to pass a closure to C this way:

func = ->(x : Int32, y : Int32, z : Int32, w : Int32) : Nil {
  foo # `func` is now a closure
  puts [x, y, z, w].join(' ')
}
foo.value.cb.call(func, 1, 2, 3, 4)
1 2 3 -1080507584
1 2 3 -1080507584

w becomes garbage and in this case happens to be the lower 32 bits of func.pointer. The workaround is to make Lib::Callback declare a Void* parameter instead of Func, and then pass func.pointer explicitly:

lib Lib
  alias Callback = Void*, Int32, Int32, Int32, Int32 ->
  # ...
end

raise ... if func.closure_data
foo.value.cb.call(func.pointer, 1, 2, 3, 4)
1 2 3 4
1 2 3 4

This suggests that there is an ABI issue where Crystal thinks foo.value.cb accepts a Crystal Proc argument with 2 pointers, when it should accept a C function pointer. But of course evaluating foo.value.cb itself gives a Crystal Proc, and there is no way to distinguish it from a non-C Proc with the same parameters. I think this is another scenario where Crystal would benefit from a separate CProc type.