I was interested in bringing Libuv to Nim as an experiment to see how it would work against native implementations. After getting it to compile correctly with Nim's build system, I was able to create a simple TCP echo server. Then, I encountered into a problem which I am still stuck with: exceptions are not getting handled at all.
To explain it a bit better, these are two snippets of code of a very simple implementation of a TCP client:
proc onConnect(req: ptr uv_connect_t; status: cint) {.cdecl.} =
let fut = cast[Future[TCPConnection]](req.data)
let ctx = cast[TCPConnection](req.handle.data)
if status != 0:
fut.fail(returnException(status))
return
let err = uv_read_start(req.handle, allocBuffer, readCb)
if err != 0:
fut.fail(returnException(err))
return
fut.complete(ctx)
asyncCheck dial("127.0.0.1", 8000.Port)
runForever()
To get this working, I've imported the module asyncfutures which does not depend on the asyncdispatch backend, and then wrote my own version of runForever which just calls uv_run with the default loop and the mode set as UV_RUN_DEFAULT. dial just resolves the IP address into an object that is accepted by libuv and then calls uv_tcp_connect which has the callback set to onConnect
So, what is the problem? After uv_read_start is called, libuv will "block" the program until data is read, and for some reason this causes any exception to not get handled until the event loop exits, either if there are no more active tasks, or by manually stopping it with uv_stop.
I initially thought about fixing it by adding a "global" exception handler, that before actually throwing the error would stop the event loop: however this doesn't seem to be possible at all Nim. Then, I've discovered if the exception backend is set to setjmp, they would get actually thrown while the loop is started.
I am wondering: Is related to a possible bug, or it's just a side effect on how the goto exception backend behaves? I never worked internally on the Nim compiler, so I have no idea on how this actually works
As I have showed on the code, I do in fact already check if an error occurs. I have tried to purposely throw an exception with also the nimTestErrorFlag, but still nothing really changes.
proc onConnect(req: ptr uv_connect_t; status: cint) {.cdecl.} =
let fut = cast[Future[TCPConnection]](req.data)
let ctx = cast[TCPConnection](req.handle.data)
if status != 0:
fut.fail(returnException(status))
return
let err = uv_read_start(req.handle, allocBuffer, readCb)
if err != 0:
fut.fail(returnException(err))
return
raise newException(Exception, "Could not read")
{.emit: "nimTestErrorFlag();".}
with this code you specified you return earlier then raising an exception
also check where the future.fail will lead. it jump into asyncCheck or something that awaits a coroutine and exception will be raised without emiting nimTestErrorFlag
remove error checking part
if err != 0:
return fut.fail()
and leave only the code below
I don't think it's really a problem because I'm sure the checks above don't generate any error, I have added the exception just to purposely throw an error in any case.
The exception is checked by asyncCheck which is part of the asyncfutures module. I have just tried to add nimTestErrorFlag on this function but it seems it doesn't change anything. When I add uv_stop it gets handled immediately, but I want to avoid to call it every time an async task raises an error
I did try to avoid possible issues: Initially, some objects used by libuv were created as a ref object and then casted to ptr (which you shouldn't do it, I know), but now alloc is always used so everything is correct. I have also replaced the memory management functions used by the library from malloc to Nim's allocators so the context of the pointers should be the same (Yes, I have tested the program with -d:useMalloc, the result is the same), and made sure ARC doesn't deallocate futures by using GC_ref ad GC_unref
Regarding the code inside the event loop: it's possible that exceptions might be messing with it, but even unhandled exceptions are not managed at all (the program doesn't quit), which shouldn't be a problem. If it matters, the variable containing the loop is a global one, and is automatically destroyed with the destroy hook
I've decided to investigate further this behavior, so I set up a very simple program that calls a C function and then a Nim one that is defined through cdecl:
{.emit: """#include <stdio.h>
#include <unistd.h>
typedef struct {
void (*callback)();
} Scallback;
int csleep(Scallback callback) {
usleep(5000000);
callback.callback();
usleep(5000000);
printf("Done sleeping\n");
return 0;
}"""}
proc callback() {.cdecl.} =
echo "Callback called"
raise newException(Exception, "Callback called")
type FCallback = proc (): void {.cdecl.}
type Scallback* {.importc: "Scallback".} = object
callback*: FCallback
proc csleep(callback: Scallback): cint {.importc.}
when isMainModule:
var cb = Scallback(callback: callback)
discard csleep(cb)
discard csleep(cb)
Callback called
Done sleeping
Callback called
Done sleeping
ctest.nim(33) ctest
ctest.nim(21) callback
Error: unhandled exception: Callback called [Exception]
When I run the program, the exception is not handled at all until both of the csleep calls are done. From what I understand, exceptions are handled only at the end of the last block of code that is not cdecl, in fact if I wrap the first csleep call inside another regular function, the exception is handled regularly when the C function returns:
{.emit: """#include <stdio.h>
#include <unistd.h>
typedef struct {
void (*callback)();
} Scallback;
int csleep(Scallback callback) {
usleep(5000000);
callback.callback();
usleep(5000000);
printf("Done sleeping\n");
return 0;
}"""}
proc callback() {.cdecl.} =
echo "Callback called"
raise newException(Exception, "Callback called")
type FCallback = proc (): void {.cdecl.}
type Scallback* {.importc: "Scallback".} = object
callback*: FCallback
proc csleep(callback: Scallback): cint {.importc.}
proc x() =
var cb = Scallback(callback: callback)
discard csleep(cb)
when isMainModule:
x()
echo "2"
var cb = Scallback(callback: callback)
discard csleep(cb)
Callback called
Done sleeping
ctest2.nim(35) ctest2
ctest2.nim(32) x
ctest2.nim(21) callback
Error: unhandled exception: Callback called [Exception]
Now, I could just get rid of cdecl completely, but since libuv requires me to provide a C function, I am forced to use it. What are the alternatives that allow me to signal exceptions to a future?
How about:
proc impl() =discard "..."
proc wrapperForLibUv(future: ???) {.cdecl.} =
try:
impl()
except:
future.setError()
But I still don't understand the problem tbh.
also try to handle errors not globally but in async function
nim
proc main() {.async.} =
try:
await dial(...)
except e:
stopLoop()
raise e
asyncCheck main()
runForever()
If anyone is interested, I think I found a possible solution.
First, I took Araq's suggestion of wrapping the actual implementation of callbacks inside a regular procedure: normally, if an exception gets thrown, it will not be propagated like the C example, however, as I said previously, if the event loop is stopped, it will.
So, I've added uv_stop inside the except block, and it seems like it's working as expected: unhandled exceptions makes the program exit, including the ones that are thrown by other futures.
proc tcpConnectionOnConnectImpl(req: ptr uv_connect_t, status: cint) =
...
self.stream = stream
fut.complete(self)
proc tcpConnectionOnConnect(req: ptr uv_connect_t, status: cint) {.cdecl.} =
try:
tcpConnectionOnConnectImpl(req, status)
except:
uv_stop(defaultLoop.loop)
raise