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