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.
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.
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 ...
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:
Perl.
.
eval "use LWP::UserAgent"
when the module
is loaded. This is required by Perl.
sv
(scalar value representing the actual
object) is passed as a parameter to the class.
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:
agent
(not
"get_agent"). This is the standard for OCaml code.
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.
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]
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.
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 sv
s
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
.
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.