Writing a high-level wrapper around a Perl library

This document discusses the theory and practice behind writing a wrapper around a typical object-oriented Perl library. We use Pl_LWP_UserAgent as an example. (This is the high-level wrapper around the LWP::UserAgent library).

Don't worry - writing wrappers is really not very hard at all. I hope that you, the reader, will write some wrappers around your favorite Perl libraries and contribute them back to perl4caml development.

First steps

I'm going to use LWP::UserAgent as my example throughout this document. Substitute that for whatever library you want to wrap up and call from OCaml. First of all make sure you have the library installed and working under Perl, and make sure you have the manual page for that library in front of you:

perldoc LWP::UserAgent

or follow this link.

Understanding what we're doing

The low-level Perl module offers two useful functions and a useful datatype which we'll be using extensively. The useful functions are:

Function name Perl equivalent Description
call_class_method $obj = LWP::UserAgent->new (args...)

Calls a static method or constructor on a class.

call_method $obj->some_method (args...)

Calls an instance method on an object.

The useful datatype is called the Perl.sv (an abstract type), which represents a scalar value in Perl (anything you would normally write in Perl with a $, including numbers, strings, references and blessed objects). To find out more about "SVs" see the perlguts(3) man page.

To see how these three things interact, let's create an LWP::UserAgent object and call a method on it:

# #load "perl4caml.cma";;
# open Perl;;
# let sv = call_class_method "LWP::UserAgent" "new" [];;
val sv : Perl.sv = <abstr>
# let agent = call_method sv "agent" [];;
val agent : Perl.sv = <abstr>
# string_of_sv agent;;
- : string = "libwww-perl/5.69"

Note how the variable sv contains the actual Perl object (an instance of LWP::UserAgent). To be quite clear, this is the equivalent of the following Perl code:

$sv = LWP::UserAgent->new ();
$agent = $sv->agent ();
print $agent;

You could actually just continue to use the low-level interface to access Perl objects directly, but there are three problems with this: firstly it's cumbersome because you have to continually convert to and from SVs; secondly it's not type safe (eg. calling string_of_sv might fail if the SV isn't actually a string); thirdly there are more pleasant ways to present this interface.

Writing a high-level wrapper around these low-level operations is what is described in the rest of this document ...

Classes and constructors

Our general plan, therefore, will be to create an OCaml class corresponding to LWP::UserAgent, which hides the implementation (the sv, the calls to call_method, and the conversion of arguments to and from SVs). We will also need to write one or more constructor function.

We will write at least one method for every method exported by the Perl interface. Sometimes we'll write two methods for each Perl method, as in the case where a Perl method is a "getter/setter":

$ua->agent([$product_id])
   Get/set the product token that is used to identify the user agent
   on the network.  The agent value is sent as the "User-Agent" header
   in the requests.

becomes two methods in the OCaml version:

class lwp_useragent sv = object (self)
  (* ... *)
  method agent : string
  method set_agent : string -> unit
end

We will also write at least one function for every constructor or static function exported from Perl.

The OCaml object itself contains the sv which corresponds to the Perl SV (ie. the actual Perl object).

Here is the shape of our class:

(** Wrapper around Perl [LWP::UserAgent] class.
  *
  * Copyright (C) 20xx your_organisation
  *
  * $ Id $
  *)

open Perl

let _ = eval "use LWP::UserAgent"

class lwp_useragent sv = object (self)

  The methods will go here ...

end

(* The "new" constructor. Note that "new" is a reserved word in OCaml. *)
let new_ ... =
  ...
  let sv = call_class_method "LWP::UserAgent" "new" [args ...] in
  new lwp_useragent sv

Any other static functions will go here ...

Notice a few things here:

  1. There is some ocamldoc describing the class.
  2. We "open Perl" to avoid having to prefix everything with Perl..
  3. We eval "use LWP::UserAgent" when the module is loaded. This is required by Perl.
  4. The sv (scalar value representing the actual object) is passed as a parameter to the class.

Writing methods

Getters and setters

Of all types of methods, getters and setters are the easiest to write. First of all, check the manual page to find out what type the slot is. You'll need to write one get method and one set method. (Rarely you'll find getters and setters which are quasi-polymorphic, for instance they can take a string or an arrayref. You'll need to think more deeply about these, because they require one set method for each type, and the get method can be complicated).

Here's our getter and setter for the agent slot, described above. The agent is a string:

  method agent =
    string_of_sv (call_method sv "agent" [])
  method set_agent v =
    call_method_void sv "agent" [sv_of_string v]

Note:

  1. The get method is just called agent (not "get_agent"). This is the standard for OCaml code.
  2. We use string_of_sv and sv_of_string to convert to and from SVs. This will ensure that the class interface will have the correct type (string), and thus be type safe as far as the calling code is concerned, and also means the caller doesn't need to worry about SVs.
  3. The set method called call_method_void which we haven't seen before. This is exactly the same as call_method except that the method is called in a "void context" - in other words, any return value is thrown away. This is slightly more efficient than ignoring the return value.

Here's another example, with a boolean slot:

  method parse_head =
    bool_of_sv (call_method sv "parse_head" [])
  method set_parse_head v =
    call_method_void sv "parse_head" [sv_of_bool v]

Ordinary methods

Other methods are perhaps simpler to wrap than getters and setters. LWP::UserAgent contains an interesting method called request (which actually runs the request).

What's particularly interesting about this method are the parameter and return value. It takes an HTTP::Request object and returns an HTTP::Response.

I have already wrapped HTTP::Request and HTTP::Response as modules Pl_HTTP_Request and Pl_HTTP_Response respectively. You should go and look at the code in those modules now.

If request requires a parameter, what should that parameter be? Naturally it should be the SV corresponding to the HTTP::Request object. To get this, I provided an #sv method on the http_request class.

And what will request return? Naturally it will return an SV which corresponds to the (newly created inside Perl) HTTP::Response object. We need to wrap this up and create a new OCaml http_response object, containing that SV.

This is what the final method looks like:

  method request (request : http_request) =
    let sv = call_method sv "request" [request#sv] in
    new http_response sv

It's actually not so complicated.

Writing constructors and static functions

Constructors are fairly simple, although the new_ function inside Pl_LWP_UserAgent is complicated by the many optional arguments which LWP::UserAgent->new can take.

Here is the guts, omitting all but one of the optional args:

let new_ ?agent (* ... *) () =
  let args = ref [] in
  let may f = function None -> () | Some v -> f v in
  may (fun v ->
	 args := sv_of_string "agent" :: sv_of_string v :: !args) agent;
(* ... *)
  let sv = call_class_method "LWP::UserAgent" "new" !args in
  new lwp_useragent sv

It works simply enough, first building up a list of svs corresponding to the arguments, then calling call_class_method to create the Perl object, then returning a constructed OCaml lwp_useragent object containing that sv.

Contributing wrappers back to perl4caml

If you write a wrapper for a Perl class, particularly one from CPAN, I urge you to contribute it back to the perl4caml development effort. Your contribution enriches the project as a whole, and makes OCaml more useful too.


Richard W.M. Jones
Last modified: Thu Oct 16 14:39:02 BST 2003