Home page > OCaml > Dynlink as dlopen..

Dynlink as dlopen..

Saturday 14 May 2011, by Toots

Dear Lazyweb,

One big issue we constantly face with liquidsoap is that most of our users have to recompile the whole software just to have support for mp3 encoding using liblame and ocaml-lame.

We have been devising two different approaches with Samuel to address this issue. I would like to expose mine here, both for documentation purposes — finding documentation and examples of how to use the dynlink module is not easy — and to maybe get some feedback..

The whole issue with dynlink is that it is capable of loading a module at run-time but cannot expose its API to the running program. Therefore, a shared module has to be defined so that it registers some functionalities when loaded.

In our case, we would like to be able to activate mp3 encoding by loading the Lame module at run-time if detected. Our requirements are the following:

  • We should be able to compile liquidsoap on a system which does not have ocaml-lame installed
  • We would like to expose the whole ocaml-lame API once loaded dynamically

The first requirement is important because we would like to distribute a canonical build of liquidsoap through debian for instance, so that if a user wants to dynamically load Lame, she only has to compile ocaml-lame. On the one hand, this reduces our workload by only having to work on one package and on the other hand, this the only case which makes sense.

Indeed, the other solution would be to compile a liquidsoap plugin against a compiled Lame module. Practically, this means that we would have to compile a liquidsoap package on a system which has lame installed. But in this case, we should simply build the sotware with lame included and forget about the shared module..

The second requirement is a natural consequence of the first one: if the shared module is built independently of liquidsoap, then it has to be provided by ocaml-lame. Then in this case, it should be more general than just exposing the functionalities needed by liquidsoap..

So, after much poking around, here is the solution I came to. I will explain it with a simple example.

Imagine that you have a module Foo which has the following interface.

(** An abstract type for the encoder. *)
type encoder

(** A function to instanciate an encoder. *)
val create : unit -> encoder

(** A function to encode. *)
val encode : encoder -> float array array -> string

The idea here is that we want to expose the whole API and make it possible for any software to be compiled without having the module Foo installed and still be able to dynalically load it at run-time. Therefore, we define the Foo_dynlink interface:

type encoder
type encoder_handler =
 { create : unit -> encoder;
    encode : encoder -> float array array -> string }
type handler = { mutable encoder_handler : encoder_handler option }
val handler : handler

But we do not implement the corresponding .ml module. This mli specifies what the program that will load dynamically Foo will have to implement.

Now, we implement a loader for Foo, called Foo_loader:

open Foo_dynlink
let () =
 let create () =
   Obj.magic (Foo.create ())
 in
 let encode enc data =
   Foo.encode (Obj.magic enc) data
 in
 handler.encoder_handler <-
   Some { create = create;
                encode = encode }

Here, we need to use Obj.magic as the two abstract types Foo.encoder and Foo_dynlink.encoder cannot be unified: Foo_dynlink will be implemented by the calling program, which will not have Foo available..

Finally, we can now write a program that dynamically loads Foo. It consists of at least two modules, Foo_dynlink, which implements the corresponding interface and another module that uses it. Let’s see foo_dynlink.ml first:

type encoder
type encoder_handler =
 { create : unit -> encoder;
    encode : encoder -> float array array -> string }
type handler = { mutable encoder_handler : encoder_handler option }
let handler = { encoder_handler = None }

Now main.ml

let foo_dyn = "/path/to/foo.cmxs"
let foo_loader = "/path/to/foo_loader.cmxs"

open Foo_dynlink

let () =
 (** First load Foo. *)
 Dynlink.loadfile foo_dyn;
 (** Now execute the loader. *)
 Dynlink.loadfile foo_loader;
 (** At this point, Foo_dynlink.handler should be populated. *)
 match handler.encoder_handler with
     | None -> Printf.printf "Dynamic loading failed..\n"
     | Some _ -> Printf.printf "Dynamic loading succeeded!!\n"

Let’s try it now!

First. make a seperate directory for Foo:

mkdir /tmp/foo
cd /tmp/foo

Then compile the module and its loader:

ocamlopt -c foo.mli
ocamlopt -shared foo.ml -o foo.cmxs
ocamlopt -c foo_dynlink.mli
ocamlopt -shared foo_loader.ml -o foo_loader.cmxs
# We do not need the compile interface for foo_dynlink at this point..
rm foo_dynlink.cmi

Now, let’s prepare another directory for the main program:

mkdir /tmp/bar
cd /tmp/bar
cp /tmp/foo/foo_dynlink.mli .

And compile it:

ocamlopt -c foo_dynlink.mli
ocamlopt -c foo_dynlink.ml
ocamlopt -c main.ml
ocamlfind ocamlopt -linkpkg -package dynlink foo_dynlink.cmx main.cmx -o bar

Of course, the paths to foo.cmxs and foo_loader.cmxs in main.ml have been changed to their actual location..

At this point, bar has been compiled without any reference to Foo. The only requirement is that it implements a module that has the interface required by foo_loader.cmxs. Now is the time to try it!

./bar
Dynamic loading succeeded!!

:-)

What do you think? Ugly or interesting?

4 Forum messages

  • Dynlink as dlopen.. Le 14 May 2011 à 17:46 , by Daniel Bünzli

    Ugly ! Because of unecessary use of Obj. But you are almost there. OCaml 3.12’s first class modules help you to solve the problem. Here’s one way of doing it, I generalized to multiple encoders.

    module Mp3 = struct
     type t
     let create () = ...
     let encode t samples = ...
    end
    module Encoder : sig
     module type T = sig
       type t
       val create : unit -> t
       val encode : t -> float array array -> string
     end

     type kind = [ `Mp3 | `Wav | `Other of string ]

     val get : kind -> (module T) option
     val set : kind -> (module T) -> unit
    end = struct
     module type T = sig
       type t
       val create : unit -> t
       val encode : t -> float array array -> string
     end

     type kind = [ `Mp3 | `Wav | `Other of string ]

     let encoders = Hashtbl.create 10
     let get enc = try Some (Hashtbl.find encoders enc) with Not_found -> None
     let set enc e = Hashtbl.replace encoders enc e
    end
    module Mp3_loader = struct
     let () = Encoder.set `Mp3 (module Mp3 : Encoder.T)
    end

    Loading procedure is as you suggest and now wherever needed

    match Encoder.get `Mp3 with None -> failwith "no support for mp3"
    | Some e ->
       let module Mp3 = (val e : Encoder.T) in
       let e = Mp3.create () in
       ...

    If you don’t need multiple encoders the loader could also just set a global references of type :

    val mp3 : (module Encoder.T) option ref

    Best,

    Daniel

    Reply to this message

    • Dynlink as dlopen.. Le 14 May 2011 à 22:18 , by Toots

      Hi Daniel and thanks for your comment!

      Indeed, the solution using first-class modules looks great and much more elegant!

      The only thing I am worried about in this case is backward compatibility. OCaml 3.12 has landed in debian unstable only recently and I do not even have it on all my dev machines..

      However, we are looking for a solution that would be mostly targeted for the debian package, starting with unstable. In the mean time, users that recompile liquidsoap may as well simply use the builtin module. From this point of view it seems acceptable to implement the feature with first-class modules... I’m leaning toward that idea but I’ll see what the other devs have to say about it.

      Again, thanks for your comment, it was greatly appreciated!

      Reply to this message

      • Dynlink as dlopen.. Le 16 May 2011 à 09:36 , by gasche

        In Daniel message, the use of first-class modules is motivated by the abstract type "t". But the use of "t" itself is not very expressive, and actually an abstract type is not really needed here : the "create" function could return the encoding function directly.

         type encoder = unit -> (float array array -> string)

         let mp3 () =
           ....
           fun t samples -> ...

         let encoder : encoder option ref = ref None

         (* use example *)
         match !encoder with
         | None -> failwith "no support for mp3"
         | Some create_encoder ->
          let encoder = create_encoder () in
          ...

        Reply to this message

        • Dynlink as dlopen.. Le 16 May 2011 à 18:03 , by toots

          Hi gasche and thanks for your comment.

          The example was actually a toy example. In reality, the API for the encoder is much more complex, with functions to set various parameters for the encoder, different encoding functions etc..

          The advantage of using first-class modules in this case is tremendous: instead of writing a wrapper for each functions, type and exception in the API, one simply has to register the module in a record/reference just like a reguar value..

          I have actually updated my current implementation, available there. I think its a very nice and elegant solution!

          Reply to this message

Reply to this article