# C bindings with callback

**URL:** <https://forum.crystal-lang.org/t/c-bindings-with-callback/6729>\
**Category:** Help & Support\
**Created:** [March 28, 2024, 7:11pm UTC](https://forum.crystal-lang.org/t/c-bindings-with-callback/6729 "2024-03-28T19:11:50Z")\
**Posts on this page:** 5\
**Page:** 1

<div class="post-metadata">

**Author:** ![nerdinand](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/nerdinand/32/2342_2.png) [@nerdinand](https://forum.crystal-lang.org/u/nerdinand)\
**Post date:** [March 28, 2024, 7:11pm UTC](https://forum.crystal-lang.org/t/c-bindings-with-callback/6729/1 "2024-03-28T19:11:50Z")

</div>

I’m trying to write some Crystal bindings for the libical C-library.

I found [Callbacks - Crystal](https://crystal-lang.org/reference/1.11/syntax_and_semantics/c_bindings/callbacks.html#passing-a-closure-to-a-c-function) and tried to use it to adapt the [icalparser\_get\_line](https://libical.github.io/libical/apidocs/icalparser_8h.html#ada2de638b68de5146ae860631709f4a3) function.

Here’s the relevant part of libical.cr:

```auto
@[Link("ical")]
lib LibIcal
  # ...
  fun get_line = "icalparser_get_line"(
    parser : LibIcal::IcalParser*, 
    line_gen_func : (LibC::Char*, LibC::SizeT, Void* -> LibC::Char*), 
    data : Void*
  ) : LibC::Char*
end

```

I then tried to use the function like so:

```auto
require "../../src/app.cr"

module Ical
  # The callback for the user doesn't have a Void*
  @@box = Pointer(Void).null

  def self.line_gen_callback(parser : LibIcal::IcalParser, &callback : ((LibC::Char*, LibC::SizeT, Void*) -> LibC::Char*))
    # Since Proc is a {Void*, Void*}, we can't turn that into a Void*, so we
    # "box" it: we allocate memory and store the Proc there
    boxed_data = Box.box(callback)

    # We must save this in Crystal-land so the GC doesn't collect it (*)
    @@box = boxed_data

    # We pass a callback that doesn't form a closure, and pass the boxed_data as
    # the callback data
    LibIcal.get_line(pointerof(parser), -> (s : LibC::Char*, size : LibC::SizeT, data : Void*) {
      p "here"
      p s
      p size
      p data
      # Now we turn data back into the Proc, using Box.unbox
      data_as_callback = Box(typeof(callback)).unbox(data)

      # And finally invoke the user's callback
      data_as_callback.call(s, size, data)
    }, boxed_data)
  end
end

parser = LibIcal.new_parser

File.open("basic.ics") do |file|
  LibIcal.set_gen_data(pointerof(parser), pointerof(file))

  line = -1

  until line == 0
    Ical.line_gen_callback(parser) do |str, size, d|
      p "there"
      line_string = file.gets
      p line_string
      if line_string.nil?
        Pointer(LibC::Char).null
      else
        Intrinsics.memcpy(str, line_string, line_string.size, false)
        str
      end
    end
  end
end

```

I’m getting the following error:

```auto
Invalid memory access (signal 11) at address 0x0
[0x102cd15c0] *Exception::CallStack::print_backtrace:Nil +104 in /Users/nerdinand/.cache/crystal/crystal-run-eval.tmp
[0x102cb7b48] ~procProc(Int32, Pointer(LibC::SiginfoT), Pointer(Void), Nil)@/opt/homebrew/Cellar/crystal/1.11.2/share/crystal/src/crystal/system/unix/signal.cr:131 +320 in /Users/nerdinand/.cache/crystal/crystal-run-eval.tmp
[0x188c83584] _sigtramp +56 in /usr/lib/system/libsystem_platform.dylib
[0x102cbcfd8] ~procProc(Pointer(UInt8), UInt64, Pointer(Void), Pointer(UInt8))@tmp/console/1711652312560_console.cr:17 +152 in /Users/nerdinand/.cache/crystal/crystal-run-eval.tmp
[0x10355e728] icalparser_get_line +216 in /opt/homebrew/Cellar/libical/3.0.17_1/lib/libical.3.0.17.dylib
[0x102e7aea8] *Ical::line_gen_callback<struct.LibIcal::IcalParser, &Proc(Pointer(UInt8), UInt64, Pointer(Void), Pointer(UInt8))>:Pointer(UInt8) +2932 in /Users/nerdinand/.cache/crystal/crystal-run-eval.tmp
[0x102c9e51c] __crystal_main +8888 in /Users/nerdinand/.cache/crystal/crystal-run-eval.tmp
[0x102e7c23c] *Crystal::main_user_code<Int32, Pointer(Pointer(UInt8))>:Nil +12 in /Users/nerdinand/.cache/crystal/crystal-run-eval.tmp
[0x102e7c1a4] *Crystal::main<Int32, Pointer(Pointer(UInt8))>:Int32 +60 in /Users/nerdinand/.cache/crystal/crystal-run-eval.tmp
[0x102ca7a8c] main +12 in /Users/nerdinand/.cache/crystal/crystal-run-eval.tmp
"here"
Pointer(UInt8)@0x16d166490
72057594037927936
Pointer(Void)@0x1031edf60

```

Any help with this is highly appreciated! I’m pretty new to Crystal, coming from Ruby and Python.

---

<div class="post-metadata">

**Author:** ![straight-shoota](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/straight-shoota/32/36_2.png) [@straight-shoota](https://forum.crystal-lang.org/u/straight-shoota)\
**Post date:** [March 28, 2024, 9:24pm UTC](https://forum.crystal-lang.org/t/c-bindings-with-callback/6729/2 "2024-03-28T21:24:19Z")

</div>

My first hunch would be that you try to copy more bytes into `str` than it accepts, i.e `line_string.size > size` (btw. correct would be `line_string.bytesize`). Dunno if that’s the cause for this particular error, but it could certainly cause an invalid memory access.

`IO#gets` takes a `limit` parameter which you could set to `size` to restrict how much bytes it reads.

---

<div class="post-metadata">

**Author:** ![nerdinand](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/nerdinand/32/2342_2.png) [@nerdinand](https://forum.crystal-lang.org/u/nerdinand)\
**Post date:** [March 28, 2024, 11:52pm UTC](https://forum.crystal-lang.org/t/c-bindings-with-callback/6729/3 "2024-03-28T23:52:15Z")

</div>

Thanks for the input! Correcting this however doesn’t fix the problem. Seeing as `"there"` isn’t in the output, it seems the error happens while calling the `icalparser_get_line` C-function, before the callback-block is even invoked…

---

<div class="post-metadata">

**Author:** ![yxhuvud](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/yxhuvud/32/27_2.png) [@yxhuvud](https://forum.crystal-lang.org/u/yxhuvud)\
**Post date:** [March 29, 2024, 12:21pm UTC](https://forum.crystal-lang.org/t/c-bindings-with-callback/6729/5 "2024-03-29T12:21:43Z")

</div>

This seems more complicated than necessary. There is no need to box if the lifetime of the object won’t be longer than the scope it is defined in. So something like

```auto
def self.line_gen_callback(parser : LibIcal::IcalParser, &callback : ((LibC::Char*, LibC::SizeT, Void*) -> LibC::Char*))
    LibIcal.get_line(pointerof(parser), callback)
  end

```

should work in this simple case.

Assuming everything is as claimed. But that is not the case, as the claim that the callback that is passed in doesn’t have a closure. But that is false - the callback refers to `file` which comes from outside the scope. One of those callbacks probably get the file back as the data argument. Instead of using file directly, use that pointer to construct the file variable.

That said, I usually don’t put functions in the data slot, but rather pointers to instances of objects, that tend to be a lot more flexible.

---

<div class="post-metadata">

**Author:** ![echo](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/echo/32/2008_2.png) [@echo](https://forum.crystal-lang.org/u/echo)\
**Post date:** [March 29, 2024, 2:21pm UTC](https://forum.crystal-lang.org/t/c-bindings-with-callback/6729/6 "2024-03-29T14:21:04Z")

</div>

> [@nerdinand](#):
>
> ```crystal
> fun get_line = "icalparser_get_line"(
> parser : LibIcal::IcalParser*, 
> line_gen_func : (LibC::Char*, LibC::SizeT, Void* -> LibC::Char*), 
> data : Void*
> ) : LibC::Char*
> 
> ```

So taking a look at the docs you shared, `icalparser_get_line` has the following signature in C:

```c
typedef char *(*icalparser_line_gen_func) (char *s, size_t size, void *d);

char* icalparser_get_line(icalparser* parser,
                          icalparser_line_gen_func line_gen_func)

```

I’m thinking a correct binding would be something like:

```crystal
# char *s = first LibC::Char*
# size_t size = LibC::SizeT
# void *d = Void*
# return type is LibC::Char*
alias ParserLineGenFunc = Proc(LibC::Char*, LibC::SizeT, Void*, LibC::Char*)

fun get_line = icalparser_get_line(parser : LibIcal::ICalParser*,
                                   line_gen_func : ParserLineGenFunc)

```

* * *

```crystal
parser = LibIcal.new_parser

File.open("basic.ics") do |file|
  LibIcal.set_gen_data(pointerof(parser), pointerof(file))
  # [snip]
end

```

I’m curious, how have you defined `LibIcal#new_parser` and `LibIcal::ICalParser`? In C, the `icalparser_new()` function returns `icalparser*`, so there shouldn’t be any need to use `pointerof(parser)`.

Also, keep in mind the `pointerof(file)`, this is your “data”.

* * *

```crystal
    LibIcal.get_line(pointerof(parser), -> (s : LibC::Char*, size : LibC::SizeT, data : Void*) {
      p "here"
      p s
      p size
      p data
      # Now we turn data back into the Proc, using Box.unbox
      data_as_callback = Box(typeof(callback)).unbox(data)

      # And finally invoke the user's callback
      data_as_callback.call(s, size, data)
    }, boxed_data)

```

Again, probably need to check the `pointerof(parser)`.

As @yxhuvud mentioned, there isn’t any need to box anything here, nor is there any need for the second callback. The `data` is the “data” you set earlier with `LibIcal#set_gen_data`, in other words, it’s your file. `LibIcal.get_line(parser, callback)` should work.

* * *

```crystal
  until line == 0
    Ical.line_gen_callback(parser) do |str, size, d|
      p "there"
      line_string = file.gets
      p line_string
      if line_string.nil?
        Pointer(LibC::Char).null
      else
        Intrinsics.memcpy(str, line_string, line_string.size, false)
        str
      end
    end
  end

```

Knowing that `data` is a `File`, you can convert it back in to something usable by combining [`Object#as(type)`](https://crystal-lang.org/api/1.11.2/Object.html#as%28type%3AClass%29-instance-method) and [`Pointer(T)`](https://crystal-lang.org/api/1.11.2/Pointer.html). It seems that you’ve fixed the other issues regarding `line_string.size` and `size`. I’d also suggest taking a look at your `until line == 0` loop condition as `line` never changes and there doesn’t seem to way out of the loop.

Let me know if you get stuck.
