skydeck

Already a member? Login.

Skydeck Blog

Stack traces in OCaml

One annoyance of OCaml (compared to Java or most any of the scripting languages) is that it does not automatically print a stack trace when your program throws an exception. To get one you have to compile with -g to include debugging information in the executable, and launch the runtime with -b so that it captures a stack trace on an exception. By default the runtime doesn’t spend time capturing the stack trace; this reflects a design choice to make exceptions very lightweight. Because exceptions are cheap it is good OCaml style to use them freely as a control-flow mechanism, while in most other languages such use is discouraged—exceptions are meant to be used only for error handling.

Once you’ve -g and -b‘d your way to a stack trace you can only get it printed to standard error, and then only if it reaches the top level. This is usually fine for a batch program, but for a long-running program like a web server it is a severe restriction. So we’ve written a small patch to the OCaml runtime that lets you get the stack trace as a string so you can show it in a web page, log it, etc. You can also turn the capturing of backtraces on and off inside the program so you incur the cost only when you need it.

(This patch is similar in spirit to Markus Mottl’s but is simpler and gives the same information that you get from the standard stack trace.)

To install the patch:

  1. download the patch and save it as backtrace.patch
  2. unpack a fresh OCaml 3.10.0 tree
  3. patch -p0 < backtrace.patch in the same directory where you unpacked OCaml.
  4. cd into the OCaml tree
  5. ./configure
  6. make core promote
  7. uncomment the lines in stdlib/printexc.mli that are marked UNCOMMENT
  8. make world.opt or whatever you normally do to build OCaml

(The reason for steps 6 and 7 is that OCaml compiles itself with precompiled tools in boot. These tools don’t know about the new functions in backtrace.c so they barf on printexc.mli. Step 6 builds a new version of the tools and puts them in boot.)

To use the new functions:

  1. Call Printexc.capture_backtrace true (any time before the exception is thrown).
  2. Make a string to write the backtrace into, e.g. with let b = String.create 8192.
  3. When you have caught an exception, immediately write the backtrace with let c = Printexc.sprint_backtrace b. Now c is the number of characters written into b, so you can get the part you want with String.sub b 0 c.
  4. When you no longer need to capture backtraces call Printexc.capture_backtrace false.

(This interface is a little New Jersey-ish—it would be nicer to get a string allocated for you rather than having to guess a big enough size, but it was very simple to implement.)

A few things to watch out for:

  1. If you are using bytecode, the runtime opens the bytecode file to find the debugging information; if your program changes directories (as does the Netplex server in Ocamlnet) it can’t find it unless you start the program with an absolute path. This isn’t a problem with native code.
  2. There is only one buffer into which backtraces are captured, so you must call Printexc.sprint_backtrace before any other exceptions are thrown.
  3. Tail calls don’t add anything to the stack so they don’t show up in the backtrace.
  4. Because of the way the backtrace is built up as the stack is unwound (see caml_stash_backtrace in backtrace.c for the details), it includes only the frames from the point at which the exception was thrown to the next enclosing exception handler. If the exception is re-raised then further frames are added.

8 Responses to “Stack traces in OCaml”

  1. igor says:

    external capture_backtrace : bool -> unit = “caml_capture_backtrace”;;
    external sprint_backtrace : string -> int = “caml_sprint_backtrace”;;

    Have this lines to be commented (in the path) and uncommented (in the step 7)?

  2. Jake says:

    No, those lines (in stdlib/printexc.ml) do not need to be commented/uncommented; since those functions are not used and don’t appear in printexc.mli, they are not added to the table of required primitives, so there is no problem linking with the bootstrap ocamlc.

    Did this not work for you?

  3. igor says:

    sorry, it was my mistake.
    it works =)

    this patch is hard enough to be included into the ocaml deb-package…

  4. Jake says:

    Cool.

    If you want to create a source tarball that incorporates the patch, do steps 1-6, which will build ocamlc, ocamldep, and ocamllex in boot/. Save these somewhere. Now do steps 1-3 again, copy the new ocamlc, ocamldep, and ocamllex into boot/, then do step 7. The resulting directory may be built in the standard way (make world etc.), so you can tar it up.

    I didn’t do this for the patch release because I didn’t want to redistribute the whole system or a binary patch.

    (You may not need ocamldep and ocamllex; I think it is only ocamlc that actually checks the primitives, but you might as well include them for uniformity.)

  5. [...] up on our earlier patch, here is a patch to show backtraces in the OCaml top level and for dynamically-loaded code (this [...]

  6. seanmcl says:

    “this reflects a design choice to make exceptions very lightweight. Because exceptions are cheap it is good OCaml style to use them freely as a control-flow mechanism, while in most other languages such use is discouraged—exceptions are meant to be used only for error handling.”

    Does Skydeck use exceptions a great deal for control flow? I'm curious, because I'm translating some Ocaml code to Haskell that uses exceptions in this way. Of course, in Haskell the exceptions must either be translated into pure functions (Maybe/option), or everything would need to live in the IO monad. I chose to keep things pure, so I can not translate the code directly. Attempting to determine which functions should return options is not always, or even often, obvious. Since exceptions are not in the types, it is very difficult to determine what functions are raising exceptions when the control flow is not explained some way or another (e.g. in comments).

    While in my SML and Ocaml code I used to use value-carrying exceptions a great deal as control mechanisms, after this experience I think I will refrain from doing so.
    In my opinion, as a reader it is much more difficult to understand exceptions than Maybes.

  7. jaked says:

    Hi Sean,

    I agree that it is hard to understand what exceptions may be raised by a function, since that is not captured in the type. (On the other hand my experience with checked exceptions in Java is that they hinder modularity; I think this is because there is no polymorphism over exceptions.)

    Using options or a result type directly is pretty painful; the monadic approach seems better to me. You might be interested in this paper, which implements exceptions handling in OCaml with an error monad (using polymorphic variants to structure exceptions) and explores how to recover the performance of native exceptions:

    http://www.univ-orleans.fr/lifo/Members/David.T...

    To answer your original question, we do use them a fair amount for control flow. But I think the situation is a bit like with references: global references can make code hard to understand, but the local use of references in code (that presents a pure interface to its callers) is OK. The same goes for exceptions; local use of exceptions can clarify code, but their global use (for control flow at least; error handling is a separate matter) can be confusing.

  8. seanmcl says:

    Hi Jake,

    I agree the monad is an improvement. I was indeed using Maybe as a monad to avoid the explicit cases everywhere. Perhaps an error monad is really what you want in more complex cases. One nice thing about Haskell is that you can write your code over an arbitrary error monad and then change the errors around without changing the code very much. For instance, you can posit a monad dealing with errors as

    class Monad m =>MonadError m where

    and then write

    MonadError m => f :: Int -> m Int
    f 0 = fail
    f n = return $ n + n

    then you can plug in Maybe, or Either Int Error, or some other error type when you figure out what you actually want.

    The polymorphic variants is a nice touch in Ocaml.

    Thanks for the response.

    Best,

    Sean