[Help] c binding of function that takes callback


#1

Hi

tldr; how do I “interface” (call functions, etc) with the outside from inside a Proc passed as a callback to a c function?

I am trying to use the wiringPi library in crystal on a raspberry pi and I have a problem with a function that requires a callback it has the following signature:

extern int wiringPiISR (int pin, int mode, void (*function)(void)) ;

What works so far is this:

module Wiringtest
  @[Link("wiringPi")]
  lib LibwiringPi
    fun wiringPiSetupSys : Int32
    fun wiringPiISR(pin : LibC::Int, edge_type : LibC::Int, f : Void -> Void) : LibC::Int
  end

  LibwiringPi.wiringPiSetupSys
  f = -> ( x : Void ) { puts "test" }
  LibwiringPi.wiringPiISR(13, 1, f )

end

loop do
  sleep 1
end

This works, and when I push a button connected to the PI , it puts “test” on the console.
What I cannot get to work is calling any functions from the proc.

When I try I get the message that I cannot pass a closure to a c function, and the Documentation contains a chapter on how to deal with this, by “boxing” the proc and passing that as Data.
(Passing a closure to a C function )

The suggested method in the doc is to Box the proc and pass it in as “user value” and unbox it inside the proc. My problem is that that does not seem to apply in my case, because the C-function that takes the callback expects that the callback neither returns nor takes any values (void (*function)(void) )

Another suggested solution was a global variable (@@something in Module) and I can access that variable from inside the proc, but I did not manage to box some block in it to access it from inside the callback function.

again a suggestion from chat was to have a look at this:
crystal-chipmunk

_cp_gather point_query : PointQueryInfo,
def point_query(point : Vect, max_distance : Number = 0, filter : ShapeFilter = ShapeFilter::ALL, &block : PointQueryInfo ->)
  LibCP.space_point_query(self, point, max_distance, filter, ->(shape, point, distance, gradient, data) {
    data.as(typeof(block)*).value.call(PointQueryInfo.new(Shape[shape], point, distance, gradient))
  }, pointerof(block))
end

And I have to admit this goes so far over my head it’s in danger of crashing into the ISS.
If anyone could help shed some light on the quote or the problem in general, I’d appreciate it very much.


#2

Might be interesting to link to the documentation as well: http://wiringpi.com/reference/priority-interrupts-and-threads/

int wiringPiISR (int pin, int edgeType, void (*function)(void)) ;

This function registers a function to received interrupts on the specified pin. The edgeType parameter is either INT_EDGE_FALLING , INT_EDGE_RISING , INT_EDGE_BOTH or INT_EDGE_SETUP . If it is INT_EDGE_SETUP then no initialisation of the pin will happen – it’s assumed that you have already setup the pin elsewhere (e.g. with the gpio program), but if you specify one of the other types, then the pin will be exported and initialised as specified. This is accomplished via a suitable call to the gpio utility program, so it need to be available.

The pin number is supplied in the current mode – native wiringPi, BCM_GPIO, physical or Sys modes.

This function will work in any mode, and does not need root privileges to work.

The function will be called when the interrupt triggers. When it is triggered, it’s cleared in the dispatcher before calling your function, so if a subsequent interrupt fires before you finish your handler, then it won’t be missed. (However it can only track one more interrupt, if more than one interrupt fires while one is being handled then they will be ignored)

This function is run at a high priority (if the program is run using sudo, or as root) and executes concurrently with the main program. It has full access to all the global variables, open file handles and so on.

So basically, the handler will be called when an interrupt triggers, which can really be anytime, I think you’ll have to be extra-careful about what you do in that interrupt handler…

Try this:

lib LibWiringPi
  fun ISR = wiringPiISR(pin : LibC::Int, edgeType : LibC::Int, fn : ->) : LibC::Int
end

module WiringPi::ISR
  class_getter some_var = "foo"
end

fun my_interrupt_handler_for_pin42
  puts WiringPi::ISR.some_var
end

LibWiringPi.ISR(42, 42, ->my_interrupt_handler_for_pin42)

my_interrupt_handler_for_pin42 is like a C function, which doesn’t have access to local vars, instance vars, etc…
WiringPi::ISR.some_var is a global variable you should be able to access from your handler.

Note: I didn’t test this code, but it should work :hopefully:


#3

I tried your code and it does work, but that basically shifts the problem if I understand this correctly because In the my_interrupt_handler function I cannot access outside functions.

Being able to access global variables is probably enough to solve the problem. I can maybe have a fiber that sleeps and checks if the global variable has changed every second. (although that really feels wrong to do that)

Is there no other way this could be handled more cleanly?
Is there some event-based example in Crystal that I could use?


#4

For others wondering, discussion continued on gitter starting at https://gitter.im/crystal-lang/crystal?at=5c46eaea1cb70a372a1ac71b


#5

You can access outer functions. In fact you can pass a callback like this:

LibWiringPi.ISR(42, 42, ->{
  puts "On callback"
})

Note that we are calling puts, which is an outer function/method.

The only thing you can’t do in these C callbacks is capture local variables or capture self because then it would be a closure and that’s not exactly a C function pointer (if you do it the compiler will try to detect it at compile-time or give an error at runtime).


#6

After some help from @bew in chat, and some digging, I ended up with this:

require "socket"

  @[Link("wiringPi")]
  lib LibWiringPi
    fun Setup = wiringPiSetupSys : LibC::Int
    fun ISR = wiringPiISR(pin : LibC::Int, edgeType : LibC::Int, fn : ->) : LibC::Int
  end

  fun my_interrupt_handler
    UNIXSocket.open("/tmp/gpio_detector.sock") do |sock|
      sock.puts "Event"
    end
  end

class Detector
  property server

  def initialize(config)

    LibWiringPi.Setup()
    LibWiringPi.ISR(config["gpioPinNumber"].as_i, 1, ->my_interrupt_handler)

    socket_file = "/tmp/gpio_detector.sock"
    if File.exists?(socket_file)
      puts "socket found"
      File.delete(socket_file)
    end
    @server = UNIXServer.new(socket_file)
  end
end

On the other end of this, I just open the same socket and continously read it. Due to the IO nature of this, it does not waste cpu by polling, but waits when nothing is available to read.

This seems to work fine for my purposes and hopefully has little impact on the smooth running of the rest of the program.

Thanks again to all replies :)


#7

The reply is frustrating to me because:

  1. It’s a horribly roundabout solution.
  2. It ignores previous comments that you can, in fact, call “outside functions”.
  3. The example still doesn’t include any real reaction to the callback (and it doesn’t do anything that couldn’t be done without the socket), so I can’t really fix it up without inventing my own continuation of your example.

I still insist on what I said in chat. You can do whatever you want in these callbacks, except accessing local variables, which includes function parameters and, most importantly, the implicit self. I assume that you were trying to call a method of “the current object”, and that binds the local variable self, which is not allowed. And then you said that you can’t access outside functions, which is not the right conclusion.

It’s not the socket itself that makes your example work, it’s the fact that it’s used as a global singleton. Dropping the usage of the socket but keeping the general layout of the code (with usage of globals) would prevent the roundtrip and wouldn’t be less flexible in any way.

So, the callback could access a global variable of type Detector and call its method.

Or, you could at least replace the file socket with Crystal’s own Channel.


#8

I realized that this is actually enough to produce equivalent code using Channel.

@[Link("wiringPi")]
lib LibWiringPi
  fun Setup = wiringPiSetupSys : LibC::Int
  fun ISR = wiringPiISR(pin : LibC::Int, edgeType : LibC::Int, fn : ->) : LibC::Int
end

module Global
  class_getter gpio_detector = Channel(String).new
end

class Detector
  property server

  def initialize(config)
    LibWiringPi.Setup()
    LibWiringPi.ISR(config["gpioPinNumber"].as_i, 1, ->{
      Global.gpio_detector.send("Event")
    })

    @channel = global.gpio_detector
  end
end

#9

Well, certainly not beautiful, but it seemed like a safe solution.

In part i decided for this approach because of a comment in chat (not sure it was you) that mentioned that I needed to be careful what I do in the callback, because it can happen at any time, triggered by an interrupt.
I don’t know anything about the internals of Crystal, and only have a vague idea of the actual stuff that happens when programs run, but It does seem to me that it could pose a problem doing complex stuff in an interupt. As I understand it, crystal uses libevent for scheduling, and I suspect that running code at arbitrary times not controlled by crystal could be problematic?

I occasionally dabble with microcontrollers, and the rule there is always to keep your ISR short.

I did actually try Channel first, but got a runntime error only saying “FAILURE” and nothing specific, so I didn’t want to get too hung up on that because I didn’t know if I was using it wrong, and tried the next best thing that came to mind.
I’ll go back and see if I can get channel to work and post the code.


#10

Ok, I tried again with channels, and used this code:

@[Link("wiringPi")]
lib LibWiringPi
  fun Setup = wiringPiSetupSys : LibC::Int
  fun ISR = wiringPiISR(pin : LibC::Int, edgeType : LibC::Int, fn : ->) : LibC::Int
end

module Global
  class_getter gpio_detector = Channel(String).new
end

class Detector
  def initialize()
    LibWiringPi.Setup()
    LibWiringPi.ISR(13, 1, ->{
      Global.gpio_detector.send("Event")
    })
  end
end

det = Detector.new
loop do
  puts Global.gpio_detector.receive
end

with this result:

pi@raspberrypi:~/mnt $ ../crystal/crystal build test.cr 
pi@raspberrypi:~/mnt $ ./test
Failed to raise an exception: FAILURE
[0x53c5c] *CallStack::print_backtrace:Int32 +100
[0x3d748] __crystal_raise +96
[0x3dcdc] *raise<Exception>:NoReturn +232
[0x3dbf4] *raise<String>:NoReturn +16
[0x62590] *Thread::current:Thread +88
[0x621ac] *Crystal::Scheduler::current_fiber:Fiber +12
[0x61630] *Fiber::current:Fiber +12
[0x8e3e8] *Channel::Unbuffered(String) +132
[0x496f8] ~procProc(Nil) +28

I think i’m using channels correctly, but this will probably tell you more that it tells me.
edit: the error happens as soon as I trigger the external interrupt (i.e. push button)


#11

Sorry, I overlooked your reply. I think this my be a special case, as the callback is triggered by an outside (hardware) interrupt. see the output using channel above.


#12

Oh, I can guess what’s happening in that example.
The library starts its own thread and runs the callback inside that.
Crystal does not support I/O in threads so Channel raises an exception. But Crystal doesn’t even support exceptions in threads so there’s a more fatal failure.

Honestly, just another point to consider this a bad library. But I’m sorry that you’re having this experience. And with all this known, falling back to the solution with sockets may be the only thing that works.

That wasn’t me and I think this doesn’t apply because the library doesn’t expose this as actual interrupts but apparently as normal function calls in a thread. Though who knows, maybe the thread still expects the handler to run quickly. But this is moot because you can’t do anything useful in a thread in Crystal currently.


#13

Yes, maybe not a bad library but certainly one coming from a very different background is suspect. (wiringPi feels very much like a visitor from the microcontroller world.)

Honestly I’m surprised I can get it to work at all, after all, this is running on a not even officially supported platform and besides, this is actually a good experience due to all the help I got.
(and of course because crystal is pretty awesome)