Plugins in OCAML with Dynlink library

I slowly continue with learning of OCAML –  as a training project I work on  simplified Map-Reduce framework (utilizing Core and Async libraries).   Here I had a need to plug  a selectable code (map reduce algorithm) to main program.   Ocaml provides Dynlink library, which can dynamically link either byte-code or native object/library to running program.  This can be utilize to create simple plugin framework as explained below.

Key function is Dynlink.loadfile, which can load .cmo/cma (bytecode) or .cmxs file (native code) from given file.  Loaded modules cannot be referenced from main program, so they must register themselves in defined extension points in main program during their load, where all code in plugin modules is executed. Key insight here (which is not well documented) is that loaded modules can reference only fully evaluated modules in main program – e.g. they cannot reference module, which is loading them.

In combination with first-class modules, we can build a plugin system with well defined interfaces. Below is simplified example how it may look like:

dtest_ep.ml:

module type PLUG =
	sig
		val hello: unit -> string
	end
	
let p = ref None
let get_plugin () : (module  PLUG)  =
	match !p with 
	| Some s -> s
	| None -> failwith "No plugin loaded"
	
	

Here we define interface PLUG  of the plugin, variable p which will hold refrerence to plugin implementation (as first class module option) and function get_plugin.

dtest.ml:

open Dtest_ep

let load_plug fname = 
  let fname = Dynlink.adapt_filename fname in
  if Sys.file_exists fname then
		try
			Dynlink.loadfile fname
		with 
		| (Dynlink.Error err) as e -> 	 print_endline ("ERROR loading plugin: " ^ (Dynlink.error_message err) ); raise e
		| _ -> failwith "Unknow error while loading plugin"
	else 
	  failwith "Plugin file does not exist"
		

let () =
	load_plug "/home/ivan/workspace/mapred/tests/_build/dtest_plug.cmo";
	let module M = (val get_plugin () : PLUG) in
	print_endline (M.hello ())

This is main program – it contain function to load plugin and in main code uses plugin as module M.

dtest_plug.ml:

open Dtest_ep

module M:PLUG =
	struct
		let hello () = "Hello, World!"
	end
	
	
let () = 
 p := Some (module M:PLUG)

And this file is implementation of the plugin.

We can compile with:

ocamlbuild -lib dynlink dtest_plug.cmo dtest.byte
ocamlbuild -lib dynlink dtest_plug.cmxs dtest.native

Plugin should be compiled against same version of interfaces as main program.

External modules in plugins

Plugin can use other external modules,  but then it gets bit more tricky,  if these modules not linked in main program –  you’ll get ‘Unknown Global X’  error  during dynamic linking of the plugin.

Solution is either to force module(s) to be linked in main program ( let _ = X.y ) or load required libraries with Dynlink.loadfile (dynamically loaded modules will be visible from the plugin, but not to main program).   Other option is to pack plugin with dependent object into one library file – I have not tested it, but I assume this should work.
I experienced Interesting behaviour when plugin ligrary contains same module as main program. In this case module from plugin library is loaded and linked to the plugin and hides module from main program.
For our sample above it means, if plugin will be packed in .cma file with Dtest_ep module object,  plugin will not be able to register itself,   because Dtest_ep from main program will be hidden from it.

Leave a Reply

Your email address will not be published. Required fields are marked *