`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).