Prevent 'n' being used as a parameter name.
[libguestfs.git] / src / generator.ml
index 2535120..aecb152 100755 (executable)
@@ -33,6 +33,7 @@
  *)
 
 #load "unix.cma";;
+#load "str.cma";;
 
 open Printf
 
@@ -115,6 +116,7 @@ type flags =
   | FishAlias of string          (* provide an alias for this cmd in guestfish *)
   | FishAction of string  (* call this function in guestfish *)
   | NotInFish            (* do not export via guestfish *)
+  | NotInDocs            (* do not add this function to documentation *)
 
 let protocol_limit_warning =
   "Because of the message protocol, there is a transfer limit 
@@ -128,19 +130,34 @@ can easily destroy all your data>."
 (* You can supply zero or as many tests as you want per API call.
  *
  * Note that the test environment has 3 block devices, of size 500MB,
- * 50MB and 10MB (respectively /dev/sda, /dev/sdb, /dev/sdc).
+ * 50MB and 10MB (respectively /dev/sda, /dev/sdb, /dev/sdc), and
+ * a fourth squashfs block device with some known files on it (/dev/sdd).
+ *
  * Note for partitioning purposes, the 500MB device has 63 cylinders.
  *
+ * The squashfs block device (/dev/sdd) comes from images/test.sqsh.
+ *
  * To be able to run the tests in a reasonable amount of time,
  * the virtual machine and block devices are reused between tests.
  * So don't try testing kill_subprocess :-x
  *
- * Between each test we umount-all and lvm-remove-all (except InitNone).
+ * Between each test we blockdev-setrw, umount-all, lvm-remove-all.
  *
  * Don't assume anything about the previous contents of the block
  * devices.  Use 'Init*' to create some initial scenarios.
+ *
+ * You can add a prerequisite clause to any individual test.  This
+ * is a run-time check, which, if it fails, causes the test to be
+ * skipped.  Useful if testing a command which might not work on
+ * all variations of libguestfs builds.  A test that has prerequisite
+ * of 'Always' is run unconditionally.
+ *
+ * In addition, packagers can skip individual tests by setting the
+ * environment variables:     eg:
+ *   SKIP_TEST_<CMD>_<NUM>=1  SKIP_TEST_COMMAND_3=1  (skips test #3 of command)
+ *   SKIP_TEST_<CMD>=1        SKIP_TEST_ZEROFREE=1   (skips all zerofree tests)
  *)
-type tests = (test_init * test) list
+type tests = (test_init * test_prereq * test) list
 and test =
     (* Run the command sequence and just expect nothing to fail. *)
   | TestRun of seq
@@ -153,6 +170,12 @@ and test =
      *)
   | TestOutputList of seq * string list
     (* Run the command sequence and expect the output of the final
+     * command to be the list of block devices (could be either
+     * "/dev/sd.." or "/dev/hd.." form - we don't check the 5th
+     * character of each string).
+     *)
+  | TestOutputListOfDevices of seq * string list
+    (* Run the command sequence and expect the output of the final
      * command to be the integer.
      *)
   | TestOutputInt of seq * int
@@ -184,6 +207,21 @@ and test_field_compare =
   | CompareFieldsIntEq of string * string
   | CompareFieldsStrEq of string * string
 
+(* Test prerequisites. *)
+and test_prereq =
+    (* Test always runs. *)
+  | Always
+    (* Test is currently disabled - eg. it fails, or it tests some
+     * unimplemented feature.
+     *)
+  | Disabled
+    (* 'string' is some C code (a function body) that should return
+     * true or false.  The test will run if the code returns true.
+     *)
+  | If of string
+    (* As for 'If' but the test runs _unless_ the code returns true. *)
+  | Unless of string
+
 (* Some initial scenarios for testing. *)
 and test_init =
     (* Do nothing, block devices could contain random stuff including
@@ -219,7 +257,81 @@ and cmd = string list
  * Apart from that, long descriptions are just perldoc paragraphs.
  *)
 
-let non_daemon_functions = [
+(* These test functions are used in the language binding tests. *)
+
+let test_all_args = [
+  String "str";
+  OptString "optstr";
+  StringList "strlist";
+  Bool "b";
+  Int "integer";
+  FileIn "filein";
+  FileOut "fileout";
+]
+
+let test_all_rets = [
+  (* except for RErr, which is tested thoroughly elsewhere *)
+  "test0rint",         RInt "valout";
+  "test0rint64",       RInt64 "valout";
+  "test0rbool",        RBool "valout";
+  "test0rconststring", RConstString "valout";
+  "test0rstring",      RString "valout";
+  "test0rstringlist",  RStringList "valout";
+  "test0rintbool",     RIntBool ("valout", "valout");
+  "test0rpvlist",      RPVList "valout";
+  "test0rvglist",      RVGList "valout";
+  "test0rlvlist",      RLVList "valout";
+  "test0rstat",        RStat "valout";
+  "test0rstatvfs",     RStatVFS "valout";
+  "test0rhashtable",   RHashtable "valout";
+]
+
+let test_functions = [
+  ("test0", (RErr, test_all_args), -1, [NotInFish; NotInDocs],
+   [],
+   "internal test function - do not use",
+   "\
+This is an internal test function which is used to test whether
+the automatically generated bindings can handle every possible
+parameter type correctly.
+
+It echos the contents of each parameter to stdout.
+
+You probably don't want to call this function.");
+] @ List.flatten (
+  List.map (
+    fun (name, ret) ->
+      [(name, (ret, [String "val"]), -1, [NotInFish; NotInDocs],
+       [],
+       "internal test function - do not use",
+       "\
+This is an internal test function which is used to test whether
+the automatically generated bindings can handle every possible
+return type correctly.
+
+It converts string C<val> to the return type.
+
+You probably don't want to call this function.");
+       (name ^ "err", (ret, []), -1, [NotInFish; NotInDocs],
+       [],
+       "internal test function - do not use",
+       "\
+This is an internal test function which is used to test whether
+the automatically generated bindings can handle every possible
+return type correctly.
+
+This function always returns an error.
+
+You probably don't want to call this function.")]
+  ) test_all_rets
+)
+
+(* non_daemon_functions are any functions which don't get processed
+ * in the daemon, eg. functions for setting and getting local
+ * configuration values.
+ *)
+
+let non_daemon_functions = test_functions @ [
   ("launch", (RErr, []), -1, [FishAlias "run"; FishAction "launch"],
    [],
    "launch the qemu subprocess",
@@ -261,7 +373,12 @@ for whatever operations you want to perform (ie. read access if you
 just want to read the image or write access if you want to modify the
 image).
 
-This is equivalent to the qemu parameter C<-drive file=filename>.");
+This is equivalent to the qemu parameter C<-drive file=filename,cache=off>.
+
+Note that this call checks for the existence of C<filename>.  This
+stops you from specifying other types of drive which are supported
+by qemu such as C<nbd:> and C<http:> URLs.  To specify those, use
+the general C<guestfs_config> call instead.");
 
   ("add_cdrom", (RErr, [String "filename"]), -1, [FishAlias "cdrom"],
    [],
@@ -269,7 +386,33 @@ This is equivalent to the qemu parameter C<-drive file=filename>.");
    "\
 This function adds a virtual CD-ROM disk image to the guest.
 
-This is equivalent to the qemu parameter C<-cdrom filename>.");
+This is equivalent to the qemu parameter C<-cdrom filename>.
+
+Note that this call checks for the existence of C<filename>.  This
+stops you from specifying other types of drive which are supported
+by qemu such as C<nbd:> and C<http:> URLs.  To specify those, use
+the general C<guestfs_config> call instead.");
+
+  ("add_drive_ro", (RErr, [String "filename"]), -1, [FishAlias "add-ro"],
+   [],
+   "add a drive in snapshot mode (read-only)",
+   "\
+This adds a drive in snapshot mode, making it effectively
+read-only.
+
+Note that writes to the device are allowed, and will be seen for
+the duration of the guestfs handle, but they are written
+to a temporary file which is discarded as soon as the guestfs
+handle is closed.  We don't currently have any method to enable
+changes to be committed, although qemu can support this.
+
+This is equivalent to the qemu parameter
+C<-drive file=filename,snapshot=on>.
+
+Note that this call checks for the existence of C<filename>.  This
+stops you from specifying other types of drive which are supported
+by qemu such as C<nbd:> and C<http:> URLs.  To specify those, use
+the general C<guestfs_config> call instead.");
 
   ("config", (RErr, [String "qemuparam"; OptString "qemuvalue"]), -1, [],
    [],
@@ -284,6 +427,29 @@ The first character of C<param> string must be a C<-> (dash).
 
 C<value> can be NULL.");
 
+  ("set_qemu", (RErr, [String "qemu"]), -1, [FishAlias "qemu"],
+   [],
+   "set the qemu binary",
+   "\
+Set the qemu binary that we will use.
+
+The default is chosen when the library was compiled by the
+configure script.
+
+You can also override this by setting the C<LIBGUESTFS_QEMU>
+environment variable.
+
+Setting C<qemu> to C<NULL> restores the default qemu binary.");
+
+  ("get_qemu", (RConstString "qemu", []), -1, [],
+   [],
+   "get the qemu binary",
+   "\
+Return the current qemu binary.
+
+This is always non-NULL.  If it wasn't set already, then this will
+return the default qemu binary name.");
+
   ("set_path", (RErr, [String "path"]), -1, [FishAlias "path"],
    [],
    "set the search path",
@@ -293,9 +459,6 @@ Set the path that libguestfs searches for kernel and initrd.img.
 The default is C<$libdir/guestfs> unless overridden by setting
 C<LIBGUESTFS_PATH> environment variable.
 
-The string C<path> is stashed in the libguestfs handle, so the caller
-must make sure it remains valid for the lifetime of the handle.
-
 Setting C<path> to C<NULL> restores the default path.");
 
   ("get_path", (RConstString "path", []), -1, [],
@@ -307,13 +470,39 @@ Return the current search path.
 This is always non-NULL.  If it wasn't set already, then this will
 return the default path.");
 
+  ("set_append", (RErr, [String "append"]), -1, [FishAlias "append"],
+   [],
+   "add options to kernel command line",
+   "\
+This function is used to add additional options to the
+guest kernel command line.
+
+The default is C<NULL> unless overridden by setting
+C<LIBGUESTFS_APPEND> environment variable.
+
+Setting C<append> to C<NULL> means I<no> additional options
+are passed (libguestfs always adds a few of its own).");
+
+  ("get_append", (RConstString "append", []), -1, [],
+   [],
+   "get the additional kernel options",
+   "\
+Return the additional kernel options which are added to the
+guest kernel command line.
+
+If C<NULL> then no options are added.");
+
   ("set_autosync", (RErr, [Bool "autosync"]), -1, [FishAlias "autosync"],
    [],
    "set autosync mode",
    "\
 If C<autosync> is true, this enables autosync.  Libguestfs will make a
-best effort attempt to run C<guestfs_sync> when the handle is closed
-(also if the program exits without closing handles).");
+best effort attempt to run C<guestfs_umount_all> followed by
+C<guestfs_sync> when the handle is closed
+(also if the program exits without closing handles).
+
+This is disabled by default (except in guestfish where it is
+enabled by default).");
 
   ("get_autosync", (RBool "autosync", []), -1, [],
    [],
@@ -380,11 +569,44 @@ This returns the current state as an opaque integer.  This is
 only useful for printing debug and internal error messages.
 
 For more information on states, see L<guestfs(3)>.");
+
+  ("set_busy", (RErr, []), -1, [NotInFish],
+   [],
+   "set state to busy",
+   "\
+This sets the state to C<BUSY>.  This is only used when implementing
+actions using the low-level API.
+
+For more information on states, see L<guestfs(3)>.");
+
+  ("set_ready", (RErr, []), -1, [NotInFish],
+   [],
+   "set state to ready",
+   "\
+This sets the state to C<READY>.  This is only used when implementing
+actions using the low-level API.
+
+For more information on states, see L<guestfs(3)>.");
+
+  ("end_busy", (RErr, []), -1, [NotInFish],
+   [],
+   "leave the busy state",
+   "\
+This sets the state to C<READY>, or if in C<CONFIG> then it leaves the
+state as is.  This is only used when implementing
+actions using the low-level API.
+
+For more information on states, see L<guestfs(3)>.");
+
 ]
 
+(* daemon_functions are any functions which cause some action
+ * to take place in the daemon.
+ *)
+
 let daemon_functions = [
   ("mount", (RErr, [String "device"; String "mountpoint"]), 1, [],
-   [InitEmpty, TestOutput (
+   [InitEmpty, Always, TestOutput (
       [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
        ["mkfs"; "ext2"; "/dev/sda1"];
        ["mount"; "/dev/sda1"; "/"];
@@ -410,7 +632,7 @@ The filesystem options C<sync> and C<noatime> are set with this
 call, in order to improve reliability.");
 
   ("sync", (RErr, []), 2, [],
-   [ InitEmpty, TestRun [["sync"]]],
+   [ InitEmpty, Always, TestRun [["sync"]]],
    "sync disks, writes are flushed through to the disk image",
    "\
 This syncs the disk, so that any writes are flushed through to the
@@ -420,7 +642,7 @@ You should always call this if you have modified a disk image, before
 closing the handle.");
 
   ("touch", (RErr, [String "path"]), 3, [],
-   [InitBasicFS, TestOutputTrue (
+   [InitBasicFS, Always, TestOutputTrue (
       [["touch"; "/new"];
        ["exists"; "/new"]])],
    "update file timestamps or create a new file",
@@ -430,7 +652,7 @@ update the timestamps on a file, or, if the file does not exist,
 to create a new zero-length file.");
 
   ("cat", (RString "content", [String "path"]), 4, [ProtocolLimitWarning],
-   [InitBasicFS, TestOutput (
+   [InitBasicFS, Always, TestOutput (
       [["write_file"; "/new"; "new file contents"; "0"];
        ["cat"; "/new"]], "new file contents")],
    "list the contents of a file",
@@ -455,7 +677,7 @@ This command is mostly useful for interactive sessions.  It
 is I<not> intended that you try to parse the output string.");
 
   ("ls", (RStringList "listing", [String "directory"]), 6, [],
-   [InitBasicFS, TestOutputList (
+   [InitBasicFS, Always, TestOutputList (
       [["touch"; "/new"];
        ["touch"; "/newer"];
        ["touch"; "/newest"];
@@ -470,8 +692,8 @@ This command is mostly useful for interactive sessions.  Programs
 should probably use C<guestfs_readdir> instead.");
 
   ("list_devices", (RStringList "devices", []), 7, [],
-   [InitEmpty, TestOutputList (
-      [["list_devices"]], ["/dev/sda"; "/dev/sdb"; "/dev/sdc"])],
+   [InitEmpty, Always, TestOutputListOfDevices (
+      [["list_devices"]], ["/dev/sda"; "/dev/sdb"; "/dev/sdc"; "/dev/sdd"])],
    "list the block devices",
    "\
 List all the block devices.
@@ -479,9 +701,9 @@ List all the block devices.
 The full block device names are returned, eg. C</dev/sda>");
 
   ("list_partitions", (RStringList "partitions", []), 8, [],
-   [InitBasicFS, TestOutputList (
+   [InitBasicFS, Always, TestOutputListOfDevices (
       [["list_partitions"]], ["/dev/sda1"]);
-    InitEmpty, TestOutputList (
+    InitEmpty, Always, TestOutputListOfDevices (
       [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ",10 ,20 ,"];
        ["list_partitions"]], ["/dev/sda1"; "/dev/sda2"; "/dev/sda3"])],
    "list the partitions",
@@ -494,9 +716,9 @@ This does not return logical volumes.  For that you will need to
 call C<guestfs_lvs>.");
 
   ("pvs", (RStringList "physvols", []), 9, [],
-   [InitBasicFSonLVM, TestOutputList (
+   [InitBasicFSonLVM, Always, TestOutputListOfDevices (
       [["pvs"]], ["/dev/sda1"]);
-    InitEmpty, TestOutputList (
+    InitEmpty, Always, TestOutputListOfDevices (
       [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ",10 ,20 ,"];
        ["pvcreate"; "/dev/sda1"];
        ["pvcreate"; "/dev/sda2"];
@@ -513,9 +735,9 @@ PVs (eg. C</dev/sda2>).
 See also C<guestfs_pvs_full>.");
 
   ("vgs", (RStringList "volgroups", []), 10, [],
-   [InitBasicFSonLVM, TestOutputList (
+   [InitBasicFSonLVM, Always, TestOutputList (
       [["vgs"]], ["VG"]);
-    InitEmpty, TestOutputList (
+    InitEmpty, Always, TestOutputList (
       [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ",10 ,20 ,"];
        ["pvcreate"; "/dev/sda1"];
        ["pvcreate"; "/dev/sda2"];
@@ -534,9 +756,9 @@ detected (eg. C<VolGroup00>).
 See also C<guestfs_vgs_full>.");
 
   ("lvs", (RStringList "logvols", []), 11, [],
-   [InitBasicFSonLVM, TestOutputList (
+   [InitBasicFSonLVM, Always, TestOutputList (
       [["lvs"]], ["/dev/VG/LV"]);
-    InitEmpty, TestOutputList (
+    InitEmpty, Always, TestOutputList (
       [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ",10 ,20 ,"];
        ["pvcreate"; "/dev/sda1"];
        ["pvcreate"; "/dev/sda2"];
@@ -579,10 +801,10 @@ List all the logical volumes detected.  This is the equivalent
 of the L<lvs(8)> command.  The \"full\" version includes all fields.");
 
   ("read_lines", (RStringList "lines", [String "path"]), 15, [],
-   [InitBasicFS, TestOutputList (
+   [InitBasicFS, Always, TestOutputList (
       [["write_file"; "/new"; "line1\r\nline2\nline3"; "0"];
        ["read_lines"; "/new"]], ["line1"; "line2"; "line3"]);
-    InitBasicFS, TestOutputList (
+    InitBasicFS, Always, TestOutputList (
       [["write_file"; "/new"; ""; "0"];
        ["read_lines"; "/new"]], [])],
    "read file as lines",
@@ -757,12 +979,12 @@ This is just a shortcut for listing C<guestfs_aug_match>
 C<path/*> and sorting the resulting nodes into alphabetical order.");
 
   ("rm", (RErr, [String "path"]), 29, [],
-   [InitBasicFS, TestRun
+   [InitBasicFS, Always, TestRun
       [["touch"; "/new"];
        ["rm"; "/new"]];
-    InitBasicFS, TestLastFail
+    InitBasicFS, Always, TestLastFail
       [["rm"; "/new"]];
-    InitBasicFS, TestLastFail
+    InitBasicFS, Always, TestLastFail
       [["mkdir"; "/new"];
        ["rm"; "/new"]]],
    "remove a file",
@@ -770,12 +992,12 @@ C<path/*> and sorting the resulting nodes into alphabetical order.");
 Remove the single file C<path>.");
 
   ("rmdir", (RErr, [String "path"]), 30, [],
-   [InitBasicFS, TestRun
+   [InitBasicFS, Always, TestRun
       [["mkdir"; "/new"];
        ["rmdir"; "/new"]];
-    InitBasicFS, TestLastFail
+    InitBasicFS, Always, TestLastFail
       [["rmdir"; "/new"]];
-    InitBasicFS, TestLastFail
+    InitBasicFS, Always, TestLastFail
       [["touch"; "/new"];
        ["rmdir"; "/new"]]],
    "remove a directory",
@@ -783,7 +1005,7 @@ Remove the single file C<path>.");
 Remove the single directory C<path>.");
 
   ("rm_rf", (RErr, [String "path"]), 31, [],
-   [InitBasicFS, TestOutputFalse
+   [InitBasicFS, Always, TestOutputFalse
       [["mkdir"; "/new"];
        ["mkdir"; "/new/foo"];
        ["touch"; "/new/foo/bar"];
@@ -796,25 +1018,32 @@ contents if its a directory.  This is like the C<rm -rf> shell
 command.");
 
   ("mkdir", (RErr, [String "path"]), 32, [],
-   [InitBasicFS, TestOutputTrue
+   [InitBasicFS, Always, TestOutputTrue
       [["mkdir"; "/new"];
        ["is_dir"; "/new"]];
-    InitBasicFS, TestLastFail
+    InitBasicFS, Always, TestLastFail
       [["mkdir"; "/new/foo/bar"]]],
    "create a directory",
    "\
 Create a directory named C<path>.");
 
   ("mkdir_p", (RErr, [String "path"]), 33, [],
-   [InitBasicFS, TestOutputTrue
+   [InitBasicFS, Always, TestOutputTrue
       [["mkdir_p"; "/new/foo/bar"];
        ["is_dir"; "/new/foo/bar"]];
-    InitBasicFS, TestOutputTrue
+    InitBasicFS, Always, TestOutputTrue
       [["mkdir_p"; "/new/foo/bar"];
        ["is_dir"; "/new/foo"]];
-    InitBasicFS, TestOutputTrue
+    InitBasicFS, Always, TestOutputTrue
       [["mkdir_p"; "/new/foo/bar"];
-       ["is_dir"; "/new"]]],
+       ["is_dir"; "/new"]];
+    (* Regression tests for RHBZ#503133: *)
+    InitBasicFS, Always, TestRun
+      [["mkdir"; "/new"];
+       ["mkdir_p"; "/new"]];
+    InitBasicFS, Always, TestLastFail
+      [["touch"; "/new"];
+       ["mkdir_p"; "/new"]]],
    "create a directory and parents",
    "\
 Create a directory named C<path>, creating any parent directories
@@ -838,10 +1067,10 @@ names, you will need to locate and parse the password file
 yourself (Augeas support makes this relatively easy).");
 
   ("exists", (RBool "existsflag", [String "path"]), 36, [],
-   [InitBasicFS, TestOutputTrue (
+   [InitBasicFS, Always, TestOutputTrue (
       [["touch"; "/new"];
        ["exists"; "/new"]]);
-    InitBasicFS, TestOutputTrue (
+    InitBasicFS, Always, TestOutputTrue (
       [["mkdir"; "/new"];
        ["exists"; "/new"]])],
    "test if file or directory exists",
@@ -852,10 +1081,10 @@ This returns C<true> if and only if there is a file, directory
 See also C<guestfs_is_file>, C<guestfs_is_dir>, C<guestfs_stat>.");
 
   ("is_file", (RBool "fileflag", [String "path"]), 37, [],
-   [InitBasicFS, TestOutputTrue (
+   [InitBasicFS, Always, TestOutputTrue (
       [["touch"; "/new"];
        ["is_file"; "/new"]]);
-    InitBasicFS, TestOutputFalse (
+    InitBasicFS, Always, TestOutputFalse (
       [["mkdir"; "/new"];
        ["is_file"; "/new"]])],
    "test if file exists",
@@ -867,10 +1096,10 @@ other objects like directories.
 See also C<guestfs_stat>.");
 
   ("is_dir", (RBool "dirflag", [String "path"]), 38, [],
-   [InitBasicFS, TestOutputFalse (
+   [InitBasicFS, Always, TestOutputFalse (
       [["touch"; "/new"];
        ["is_dir"; "/new"]]);
-    InitBasicFS, TestOutputTrue (
+    InitBasicFS, Always, TestOutputTrue (
       [["mkdir"; "/new"];
        ["is_dir"; "/new"]])],
    "test if file exists",
@@ -882,7 +1111,7 @@ other objects like files.
 See also C<guestfs_stat>.");
 
   ("pvcreate", (RErr, [String "device"]), 39, [],
-   [InitEmpty, TestOutputList (
+   [InitEmpty, Always, TestOutputListOfDevices (
       [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ",10 ,20 ,"];
        ["pvcreate"; "/dev/sda1"];
        ["pvcreate"; "/dev/sda2"];
@@ -895,7 +1124,7 @@ where C<device> should usually be a partition name such
 as C</dev/sda1>.");
 
   ("vgcreate", (RErr, [String "volgroup"; StringList "physvols"]), 40, [],
-   [InitEmpty, TestOutputList (
+   [InitEmpty, Always, TestOutputList (
       [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ",10 ,20 ,"];
        ["pvcreate"; "/dev/sda1"];
        ["pvcreate"; "/dev/sda2"];
@@ -909,7 +1138,7 @@ This creates an LVM volume group called C<volgroup>
 from the non-empty list of physical volumes C<physvols>.");
 
   ("lvcreate", (RErr, [String "logvol"; String "volgroup"; Int "mbytes"]), 41, [],
-   [InitEmpty, TestOutputList (
+   [InitEmpty, Always, TestOutputList (
       [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ",10 ,20 ,"];
        ["pvcreate"; "/dev/sda1"];
        ["pvcreate"; "/dev/sda2"];
@@ -930,7 +1159,7 @@ This creates an LVM volume group called C<logvol>
 on the volume group C<volgroup>, with C<size> megabytes.");
 
   ("mkfs", (RErr, [String "fstype"; String "device"]), 42, [],
-   [InitEmpty, TestOutput (
+   [InitEmpty, Always, TestOutput (
       [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
        ["mkfs"; "ext2"; "/dev/sda1"];
        ["mount"; "/dev/sda1"; "/"];
@@ -939,7 +1168,7 @@ on the volume group C<volgroup>, with C<size> megabytes.");
    "make a filesystem",
    "\
 This creates a filesystem on C<device> (usually a partition
-of LVM logical volume).  The filesystem type is C<fstype>, for
+or LVM logical volume).  The filesystem type is C<fstype>, for
 example C<ext3>.");
 
   ("sfdisk", (RErr, [String "device";
@@ -966,25 +1195,27 @@ information refer to the L<sfdisk(8)> manpage.
 
 To create a single partition occupying the whole disk, you would
 pass C<lines> as a single element list, when the single element being
-the string C<,> (comma).");
+the string C<,> (comma).
+
+See also: C<guestfs_sfdisk_l>, C<guestfs_sfdisk_N>");
 
   ("write_file", (RErr, [String "path"; String "content"; Int "size"]), 44, [ProtocolLimitWarning],
-   [InitBasicFS, TestOutput (
+   [InitBasicFS, Always, TestOutput (
       [["write_file"; "/new"; "new file contents"; "0"];
        ["cat"; "/new"]], "new file contents");
-    InitBasicFS, TestOutput (
+    InitBasicFS, Always, TestOutput (
       [["write_file"; "/new"; "\nnew file contents\n"; "0"];
        ["cat"; "/new"]], "\nnew file contents\n");
-    InitBasicFS, TestOutput (
+    InitBasicFS, Always, TestOutput (
       [["write_file"; "/new"; "\n\n"; "0"];
        ["cat"; "/new"]], "\n\n");
-    InitBasicFS, TestOutput (
+    InitBasicFS, Always, TestOutput (
       [["write_file"; "/new"; ""; "0"];
        ["cat"; "/new"]], "");
-    InitBasicFS, TestOutput (
+    InitBasicFS, Always, TestOutput (
       [["write_file"; "/new"; "\n\n\n"; "0"];
        ["cat"; "/new"]], "\n\n\n");
-    InitBasicFS, TestOutput (
+    InitBasicFS, Always, TestOutput (
       [["write_file"; "/new"; "\n"; "0"];
        ["cat"; "/new"]], "\n")],
    "create a file",
@@ -995,15 +1226,20 @@ with length C<size>.
 
 As a special case, if C<size> is C<0>
 then the length is calculated using C<strlen> (so in this case
-the content cannot contain embedded ASCII NULs).");
+the content cannot contain embedded ASCII NULs).
+
+I<NB.> Owing to a bug, writing content containing ASCII NUL
+characters does I<not> work, even if the length is specified.
+We hope to resolve this bug in a future version.  In the meantime
+use C<guestfs_upload>.");
 
   ("umount", (RErr, [String "pathordevice"]), 45, [FishAlias "unmount"],
-   [InitEmpty, TestOutputList (
+   [InitEmpty, Always, TestOutputListOfDevices (
       [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
        ["mkfs"; "ext2"; "/dev/sda1"];
        ["mount"; "/dev/sda1"; "/"];
        ["mounts"]], ["/dev/sda1"]);
-    InitEmpty, TestOutputList (
+    InitEmpty, Always, TestOutputList (
       [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
        ["mkfs"; "ext2"; "/dev/sda1"];
        ["mount"; "/dev/sda1"; "/"];
@@ -1016,7 +1252,7 @@ specified either by its mountpoint (path) or the device which
 contains the filesystem.");
 
   ("mounts", (RStringList "devices", []), 46, [],
-   [InitBasicFS, TestOutputList (
+   [InitBasicFS, Always, TestOutputListOfDevices (
       [["mounts"]], ["/dev/sda1"])],
    "show mounted filesystems",
    "\
@@ -1026,8 +1262,22 @@ the list of devices (eg. C</dev/sda1>, C</dev/VG/LV>).
 Some internal mounts are not shown.");
 
   ("umount_all", (RErr, []), 47, [FishAlias "unmount-all"],
-   [InitBasicFS, TestOutputList (
+   [InitBasicFS, Always, TestOutputList (
       [["umount_all"];
+       ["mounts"]], []);
+    (* check that umount_all can unmount nested mounts correctly: *)
+    InitEmpty, Always, TestOutputList (
+      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ",10 ,20 ,"];
+       ["mkfs"; "ext2"; "/dev/sda1"];
+       ["mkfs"; "ext2"; "/dev/sda2"];
+       ["mkfs"; "ext2"; "/dev/sda3"];
+       ["mount"; "/dev/sda1"; "/"];
+       ["mkdir"; "/mp1"];
+       ["mount"; "/dev/sda2"; "/mp1"];
+       ["mkdir"; "/mp1/mp2"];
+       ["mount"; "/dev/sda3"; "/mp1/mp2"];
+       ["mkdir"; "/mp1/mp2/mp3"];
+       ["umount_all"];
        ["mounts"]], [])],
    "unmount all filesystems",
    "\
@@ -1043,13 +1293,13 @@ This command removes all LVM logical volumes, volume groups
 and physical volumes.");
 
   ("file", (RString "description", [String "path"]), 49, [],
-   [InitBasicFS, TestOutput (
+   [InitBasicFS, Always, TestOutput (
       [["touch"; "/new"];
        ["file"; "/new"]], "empty");
-    InitBasicFS, TestOutput (
+    InitBasicFS, Always, TestOutput (
       [["write_file"; "/new"; "some content\n"; "0"];
        ["file"; "/new"]], "ASCII text");
-    InitBasicFS, TestLastFail (
+    InitBasicFS, Always, TestLastFail (
       [["file"; "/nofile"]])],
    "determine file type",
    "\
@@ -1061,8 +1311,55 @@ The exact command which runs is C<file -bsL path>.  Note in
 particular that the filename is not prepended to the output
 (the C<-b> option).");
 
-  ("command", (RString "output", [StringList "arguments"]), 50, [],
-   [], (* XXX how to test? *)
+  ("command", (RString "output", [StringList "arguments"]), 50, [ProtocolLimitWarning],
+   [InitBasicFS, Always, TestOutput (
+      [["upload"; "test-command"; "/test-command"];
+       ["chmod"; "493"; "/test-command"];
+       ["command"; "/test-command 1"]], "Result1");
+    InitBasicFS, Always, TestOutput (
+      [["upload"; "test-command"; "/test-command"];
+       ["chmod"; "493"; "/test-command"];
+       ["command"; "/test-command 2"]], "Result2\n");
+    InitBasicFS, Always, TestOutput (
+      [["upload"; "test-command"; "/test-command"];
+       ["chmod"; "493"; "/test-command"];
+       ["command"; "/test-command 3"]], "\nResult3");
+    InitBasicFS, Always, TestOutput (
+      [["upload"; "test-command"; "/test-command"];
+       ["chmod"; "493"; "/test-command"];
+       ["command"; "/test-command 4"]], "\nResult4\n");
+    InitBasicFS, Always, TestOutput (
+      [["upload"; "test-command"; "/test-command"];
+       ["chmod"; "493"; "/test-command"];
+       ["command"; "/test-command 5"]], "\nResult5\n\n");
+    InitBasicFS, Always, TestOutput (
+      [["upload"; "test-command"; "/test-command"];
+       ["chmod"; "493"; "/test-command"];
+       ["command"; "/test-command 6"]], "\n\nResult6\n\n");
+    InitBasicFS, Always, TestOutput (
+      [["upload"; "test-command"; "/test-command"];
+       ["chmod"; "493"; "/test-command"];
+       ["command"; "/test-command 7"]], "");
+    InitBasicFS, Always, TestOutput (
+      [["upload"; "test-command"; "/test-command"];
+       ["chmod"; "493"; "/test-command"];
+       ["command"; "/test-command 8"]], "\n");
+    InitBasicFS, Always, TestOutput (
+      [["upload"; "test-command"; "/test-command"];
+       ["chmod"; "493"; "/test-command"];
+       ["command"; "/test-command 9"]], "\n\n");
+    InitBasicFS, Always, TestOutput (
+      [["upload"; "test-command"; "/test-command"];
+       ["chmod"; "493"; "/test-command"];
+       ["command"; "/test-command 10"]], "Result10-1\nResult10-2\n");
+    InitBasicFS, Always, TestOutput (
+      [["upload"; "test-command"; "/test-command"];
+       ["chmod"; "493"; "/test-command"];
+       ["command"; "/test-command 11"]], "Result11-1\nResult11-2");
+    InitBasicFS, Always, TestLastFail (
+      [["upload"; "test-command"; "/test-command"];
+       ["chmod"; "493"; "/test-command"];
+       ["command"; "/test-command"]])],
    "run a command from the guest filesystem",
    "\
 This call runs a command from the guest filesystem.  The
@@ -1073,7 +1370,16 @@ or compatible processor architecture).
 The single parameter is an argv-style list of arguments.
 The first element is the name of the program to run.
 Subsequent elements are parameters.  The list must be
-non-empty (ie. must contain a program name).
+non-empty (ie. must contain a program name).  Note that
+the command runs directly, and is I<not> invoked via
+the shell (see C<guestfs_sh>).
+
+The return value is anything printed to I<stdout> by
+the command.
+
+If the command returns a non-zero exit status, then
+this function returns an error message.  The error message
+string is the content of I<stderr> from the command.
 
 The C<$PATH> environment variable will contain at least
 C</usr/bin> and C</bin>.  If you require a program from
@@ -1086,15 +1392,60 @@ correct places.  It is the caller's responsibility to ensure
 all filesystems that are needed are mounted at the right
 locations.");
 
-  ("command_lines", (RStringList "lines", [StringList "arguments"]), 51, [],
-   [], (* XXX how to test? *)
+  ("command_lines", (RStringList "lines", [StringList "arguments"]), 51, [ProtocolLimitWarning],
+   [InitBasicFS, Always, TestOutputList (
+      [["upload"; "test-command"; "/test-command"];
+       ["chmod"; "493"; "/test-command"];
+       ["command_lines"; "/test-command 1"]], ["Result1"]);
+    InitBasicFS, Always, TestOutputList (
+      [["upload"; "test-command"; "/test-command"];
+       ["chmod"; "493"; "/test-command"];
+       ["command_lines"; "/test-command 2"]], ["Result2"]);
+    InitBasicFS, Always, TestOutputList (
+      [["upload"; "test-command"; "/test-command"];
+       ["chmod"; "493"; "/test-command"];
+       ["command_lines"; "/test-command 3"]], ["";"Result3"]);
+    InitBasicFS, Always, TestOutputList (
+      [["upload"; "test-command"; "/test-command"];
+       ["chmod"; "493"; "/test-command"];
+       ["command_lines"; "/test-command 4"]], ["";"Result4"]);
+    InitBasicFS, Always, TestOutputList (
+      [["upload"; "test-command"; "/test-command"];
+       ["chmod"; "493"; "/test-command"];
+       ["command_lines"; "/test-command 5"]], ["";"Result5";""]);
+    InitBasicFS, Always, TestOutputList (
+      [["upload"; "test-command"; "/test-command"];
+       ["chmod"; "493"; "/test-command"];
+       ["command_lines"; "/test-command 6"]], ["";"";"Result6";""]);
+    InitBasicFS, Always, TestOutputList (
+      [["upload"; "test-command"; "/test-command"];
+       ["chmod"; "493"; "/test-command"];
+       ["command_lines"; "/test-command 7"]], []);
+    InitBasicFS, Always, TestOutputList (
+      [["upload"; "test-command"; "/test-command"];
+       ["chmod"; "493"; "/test-command"];
+       ["command_lines"; "/test-command 8"]], [""]);
+    InitBasicFS, Always, TestOutputList (
+      [["upload"; "test-command"; "/test-command"];
+       ["chmod"; "493"; "/test-command"];
+       ["command_lines"; "/test-command 9"]], ["";""]);
+    InitBasicFS, Always, TestOutputList (
+      [["upload"; "test-command"; "/test-command"];
+       ["chmod"; "493"; "/test-command"];
+       ["command_lines"; "/test-command 10"]], ["Result10-1";"Result10-2"]);
+    InitBasicFS, Always, TestOutputList (
+      [["upload"; "test-command"; "/test-command"];
+       ["chmod"; "493"; "/test-command"];
+       ["command_lines"; "/test-command 11"]], ["Result11-1";"Result11-2"])],
    "run a command, returning lines",
    "\
 This is the same as C<guestfs_command>, but splits the
-result into a list of lines.");
+result into a list of lines.
+
+See also: C<guestfs_sh_lines>");
 
   ("stat", (RStat "statbuf", [String "path"]), 52, [],
-   [InitBasicFS, TestOutputStruct (
+   [InitBasicFS, Always, TestOutputStruct (
       [["touch"; "/new"];
        ["stat"; "/new"]], [CompareWithInt ("size", 0)])],
    "get file information",
@@ -1104,7 +1455,7 @@ Returns file information for the given C<path>.
 This is the same as the C<stat(2)> system call.");
 
   ("lstat", (RStat "statbuf", [String "path"]), 53, [],
-   [InitBasicFS, TestOutputStruct (
+   [InitBasicFS, Always, TestOutputStruct (
       [["touch"; "/new"];
        ["lstat"; "/new"]], [CompareWithInt ("size", 0)])],
    "get file information for a symbolic link",
@@ -1118,7 +1469,7 @@ refers to.
 This is the same as the C<lstat(2)> system call.");
 
   ("statvfs", (RStatVFS "statbuf", [String "path"]), 54, [],
-   [InitBasicFS, TestOutputStruct (
+   [InitBasicFS, Always, TestOutputStruct (
       [["statvfs"; "/"]], [CompareWithInt ("bfree", 487702);
                           CompareWithInt ("blocks", 490020);
                           CompareWithInt ("bsize", 1024)])],
@@ -1132,10 +1483,10 @@ This is the same as the C<statvfs(2)> system call.");
 
   ("tune2fs_l", (RHashtable "superblock", [String "device"]), 55, [],
    [], (* XXX test *)
-   "get ext2/ext3 superblock details",
+   "get ext2/ext3/ext4 superblock details",
    "\
-This returns the contents of the ext2 or ext3 filesystem superblock
-on C<device>.
+This returns the contents of the ext2, ext3 or ext4 filesystem
+superblock on C<device>.
 
 It is the same as running C<tune2fs -l device>.  See L<tune2fs(8)>
 manpage for more details.  The list of fields returned isn't
@@ -1143,7 +1494,7 @@ clearly defined, and depends on both the version of C<tune2fs>
 that libguestfs was built against, and the filesystem itself.");
 
   ("blockdev_setro", (RErr, [String "device"]), 56, [],
-   [InitEmpty, TestOutputTrue (
+   [InitEmpty, Always, TestOutputTrue (
       [["blockdev_setro"; "/dev/sda"];
        ["blockdev_getro"; "/dev/sda"]])],
    "set block device to read-only",
@@ -1153,7 +1504,7 @@ Sets the block device named C<device> to read-only.
 This uses the L<blockdev(8)> command.");
 
   ("blockdev_setrw", (RErr, [String "device"]), 57, [],
-   [InitEmpty, TestOutputFalse (
+   [InitEmpty, Always, TestOutputFalse (
       [["blockdev_setrw"; "/dev/sda"];
        ["blockdev_getro"; "/dev/sda"]])],
    "set block device to read-write",
@@ -1163,7 +1514,7 @@ Sets the block device named C<device> to read-write.
 This uses the L<blockdev(8)> command.");
 
   ("blockdev_getro", (RBool "ro", [String "device"]), 58, [],
-   [InitEmpty, TestOutputTrue (
+   [InitEmpty, Always, TestOutputTrue (
       [["blockdev_setro"; "/dev/sda"];
        ["blockdev_getro"; "/dev/sda"]])],
    "is block device set to read-only",
@@ -1174,7 +1525,7 @@ Returns a boolean indicating if the block device is read-only
 This uses the L<blockdev(8)> command.");
 
   ("blockdev_getss", (RInt "sectorsize", [String "device"]), 59, [],
-   [InitEmpty, TestOutputInt (
+   [InitEmpty, Always, TestOutputInt (
       [["blockdev_getss"; "/dev/sda"]], 512)],
    "get sectorsize of block device",
    "\
@@ -1187,7 +1538,7 @@ for that).
 This uses the L<blockdev(8)> command.");
 
   ("blockdev_getbsz", (RInt "blocksize", [String "device"]), 60, [],
-   [InitEmpty, TestOutputInt (
+   [InitEmpty, Always, TestOutputInt (
       [["blockdev_getbsz"; "/dev/sda"]], 4096)],
    "get blocksize of block device",
    "\
@@ -1210,7 +1561,7 @@ I<filesystem block size>).
 This uses the L<blockdev(8)> command.");
 
   ("blockdev_getsz", (RInt64 "sizeinsectors", [String "device"]), 62, [],
-   [InitEmpty, TestOutputInt (
+   [InitEmpty, Always, TestOutputInt (
       [["blockdev_getsz"; "/dev/sda"]], 1024000)],
    "get total size of device in 512-byte sectors",
    "\
@@ -1224,7 +1575,7 @@ useful I<size in bytes>.
 This uses the L<blockdev(8)> command.");
 
   ("blockdev_getsize64", (RInt64 "sizeinbytes", [String "device"]), 63, [],
-   [InitEmpty, TestOutputInt (
+   [InitEmpty, Always, TestOutputInt (
       [["blockdev_getsize64"; "/dev/sda"]], 524288000)],
    "get total size of device in bytes",
    "\
@@ -1235,7 +1586,7 @@ See also C<guestfs_blockdev_getsz>.
 This uses the L<blockdev(8)> command.");
 
   ("blockdev_flushbufs", (RErr, [String "device"]), 64, [],
-   [InitEmpty, TestRun
+   [InitEmpty, Always, TestRun
       [["blockdev_flushbufs"; "/dev/sda"]]],
    "flush device buffers",
    "\
@@ -1245,7 +1596,7 @@ with C<device>.
 This uses the L<blockdev(8)> command.");
 
   ("blockdev_rereadpt", (RErr, [String "device"]), 65, [],
-   [InitEmpty, TestRun
+   [InitEmpty, Always, TestRun
       [["blockdev_rereadpt"; "/dev/sda"]]],
    "reread partition table",
    "\
@@ -1253,9 +1604,11 @@ Reread the partition table on C<device>.
 
 This uses the L<blockdev(8)> command.");
 
-(*
   ("upload", (RErr, [FileIn "filename"; String "remotefilename"]), 66, [],
-   [],
+   [InitBasicFS, Always, TestOutput (
+      (* Pick a file from cwd which isn't likely to change. *)
+    [["upload"; "../COPYING.LIB"; "/COPYING.LIB"];
+     ["checksum"; "md5"; "/COPYING.LIB"]], "e3eda01d9815f8d24aae2dbd89b68b06")],
    "upload a file from the local machine",
    "\
 Upload local file C<filename> to C<remotefilename> on the
@@ -1266,7 +1619,12 @@ C<filename> can also be a named pipe.
 See also C<guestfs_download>.");
 
   ("download", (RErr, [String "remotefilename"; FileOut "filename"]), 67, [],
-   [],
+   [InitBasicFS, Always, TestOutput (
+      (* Pick a file from cwd which isn't likely to change. *)
+    [["upload"; "../COPYING.LIB"; "/COPYING.LIB"];
+     ["download"; "/COPYING.LIB"; "testdownload.tmp"];
+     ["upload"; "testdownload.tmp"; "/upload"];
+     ["checksum"; "md5"; "/upload"]], "e3eda01d9815f8d24aae2dbd89b68b06")],
    "download a file to the local machine",
    "\
 Download file C<remotefilename> and save it as C<filename>
@@ -1275,3197 +1633,4771 @@ on the local machine.
 C<filename> can also be a named pipe.
 
 See also C<guestfs_upload>, C<guestfs_cat>.");
-*)
 
-]
+  ("checksum", (RString "checksum", [String "csumtype"; String "path"]), 68, [],
+   [InitBasicFS, Always, TestOutput (
+      [["write_file"; "/new"; "test\n"; "0"];
+       ["checksum"; "crc"; "/new"]], "935282863");
+    InitBasicFS, Always, TestLastFail (
+      [["checksum"; "crc"; "/new"]]);
+    InitBasicFS, Always, TestOutput (
+      [["write_file"; "/new"; "test\n"; "0"];
+       ["checksum"; "md5"; "/new"]], "d8e8fca2dc0f896fd7cb4cb0031ba249");
+    InitBasicFS, Always, TestOutput (
+      [["write_file"; "/new"; "test\n"; "0"];
+       ["checksum"; "sha1"; "/new"]], "4e1243bd22c66e76c2ba9eddc1f91394e57f9f83");
+    InitBasicFS, Always, TestOutput (
+      [["write_file"; "/new"; "test\n"; "0"];
+       ["checksum"; "sha224"; "/new"]], "52f1bf093f4b7588726035c176c0cdb4376cfea53819f1395ac9e6ec");
+    InitBasicFS, Always, TestOutput (
+      [["write_file"; "/new"; "test\n"; "0"];
+       ["checksum"; "sha256"; "/new"]], "f2ca1bb6c7e907d06dafe4687e579fce76b37e4e93b7605022da52e6ccc26fd2");
+    InitBasicFS, Always, TestOutput (
+      [["write_file"; "/new"; "test\n"; "0"];
+       ["checksum"; "sha384"; "/new"]], "109bb6b5b6d5547c1ce03c7a8bd7d8f80c1cb0957f50c4f7fda04692079917e4f9cad52b878f3d8234e1a170b154b72d");
+    InitBasicFS, Always, TestOutput (
+      [["write_file"; "/new"; "test\n"; "0"];
+       ["checksum"; "sha512"; "/new"]], "0e3e75234abc68f4378a86b3f4b32a198ba301845b0cd6e50106e874345700cc6663a86c1ea125dc5e92be17c98f9a0f85ca9d5f595db2012f7cc3571945c123");
+    InitBasicFS, Always, TestOutput (
+      (* RHEL 5 thinks this is an HFS+ filesystem unless we give
+       * the type explicitly.
+       *)
+      [["mount_vfs"; "ro"; "squashfs"; "/dev/sdd"; "/"];
+       ["checksum"; "md5"; "/known-3"]], "46d6ca27ee07cdc6fa99c2e138cc522c")],
+   "compute MD5, SHAx or CRC checksum of file",
+   "\
+This call computes the MD5, SHAx or CRC checksum of the
+file named C<path>.
 
-let all_functions = non_daemon_functions @ daemon_functions
+The type of checksum to compute is given by the C<csumtype>
+parameter which must have one of the following values:
 
-(* In some places we want the functions to be displayed sorted
- * alphabetically, so this is useful:
- *)
-let all_functions_sorted =
-  List.sort (fun (n1,_,_,_,_,_,_) (n2,_,_,_,_,_,_) ->
-              compare n1 n2) all_functions
+=over 4
 
-(* Column names and types from LVM PVs/VGs/LVs. *)
-let pv_cols = [
-  "pv_name", `String;
-  "pv_uuid", `UUID;
-  "pv_fmt", `String;
-  "pv_size", `Bytes;
-  "dev_size", `Bytes;
-  "pv_free", `Bytes;
-  "pv_used", `Bytes;
-  "pv_attr", `String (* XXX *);
-  "pv_pe_count", `Int;
-  "pv_pe_alloc_count", `Int;
-  "pv_tags", `String;
-  "pe_start", `Bytes;
-  "pv_mda_count", `Int;
-  "pv_mda_free", `Bytes;
-(* Not in Fedora 10:
-  "pv_mda_size", `Bytes;
-*)
-]
-let vg_cols = [
-  "vg_name", `String;
-  "vg_uuid", `UUID;
-  "vg_fmt", `String;
-  "vg_attr", `String (* XXX *);
-  "vg_size", `Bytes;
-  "vg_free", `Bytes;
-  "vg_sysid", `String;
-  "vg_extent_size", `Bytes;
-  "vg_extent_count", `Int;
-  "vg_free_count", `Int;
-  "max_lv", `Int;
-  "max_pv", `Int;
-  "pv_count", `Int;
-  "lv_count", `Int;
-  "snap_count", `Int;
-  "vg_seqno", `Int;
-  "vg_tags", `String;
-  "vg_mda_count", `Int;
-  "vg_mda_free", `Bytes;
-(* Not in Fedora 10:
-  "vg_mda_size", `Bytes;
-*)
-]
-let lv_cols = [
-  "lv_name", `String;
-  "lv_uuid", `UUID;
-  "lv_attr", `String (* XXX *);
-  "lv_major", `Int;
-  "lv_minor", `Int;
-  "lv_kernel_major", `Int;
-  "lv_kernel_minor", `Int;
-  "lv_size", `Bytes;
-  "seg_count", `Int;
-  "origin", `String;
-  "snap_percent", `OptPercent;
-  "copy_percent", `OptPercent;
-  "move_pv", `String;
-  "lv_tags", `String;
-  "mirror_log", `String;
-  "modules", `String;
-]
+=item C<crc>
 
-(* Column names and types from stat structures.
- * NB. Can't use things like 'st_atime' because glibc header files
- * define some of these as macros.  Ugh.
- *)
-let stat_cols = [
-  "dev", `Int;
-  "ino", `Int;
-  "mode", `Int;
-  "nlink", `Int;
-  "uid", `Int;
-  "gid", `Int;
-  "rdev", `Int;
-  "size", `Int;
-  "blksize", `Int;
-  "blocks", `Int;
-  "atime", `Int;
-  "mtime", `Int;
-  "ctime", `Int;
-]
-let statvfs_cols = [
-  "bsize", `Int;
-  "frsize", `Int;
-  "blocks", `Int;
-  "bfree", `Int;
-  "bavail", `Int;
-  "files", `Int;
-  "ffree", `Int;
-  "favail", `Int;
-  "fsid", `Int;
-  "flag", `Int;
-  "namemax", `Int;
-]
+Compute the cyclic redundancy check (CRC) specified by POSIX
+for the C<cksum> command.
 
-(* Useful functions.
- * Note we don't want to use any external OCaml libraries which
- * makes this a bit harder than it should be.
- *)
-let failwithf fs = ksprintf failwith fs
+=item C<md5>
 
-let replace_char s c1 c2 =
-  let s2 = String.copy s in
-  let r = ref false in
-  for i = 0 to String.length s2 - 1 do
-    if String.unsafe_get s2 i = c1 then (
-      String.unsafe_set s2 i c2;
-      r := true
-    )
-  done;
-  if not !r then s else s2
+Compute the MD5 hash (using the C<md5sum> program).
 
-let isspace c =
-  c = ' '
-  (* || c = '\f' *) || c = '\n' || c = '\r' || c = '\t' (* || c = '\v' *)
+=item C<sha1>
 
-let triml ?(test = isspace) str =
-  let i = ref 0 in
-  let n = ref (String.length str) in
-  while !n > 0 && test str.[!i]; do
-    decr n;
-    incr i
-  done;
-  if !i = 0 then str
-  else String.sub str !i !n
+Compute the SHA1 hash (using the C<sha1sum> program).
 
-let trimr ?(test = isspace) str =
-  let n = ref (String.length str) in
-  while !n > 0 && test str.[!n-1]; do
-    decr n
-  done;
-  if !n = String.length str then str
-  else String.sub str 0 !n
+=item C<sha224>
 
-let trim ?(test = isspace) str =
-  trimr ~test (triml ~test str)
+Compute the SHA224 hash (using the C<sha224sum> program).
 
-let rec find s sub =
-  let len = String.length s in
-  let sublen = String.length sub in
-  let rec loop i =
-    if i <= len-sublen then (
-      let rec loop2 j =
-       if j < sublen then (
-         if s.[i+j] = sub.[j] then loop2 (j+1)
-         else -1
-       ) else
-         i (* found *)
-      in
-      let r = loop2 0 in
-      if r = -1 then loop (i+1) else r
-    ) else
-      -1 (* not found *)
-  in
-  loop 0
+=item C<sha256>
 
-let rec replace_str s s1 s2 =
-  let len = String.length s in
-  let sublen = String.length s1 in
-  let i = find s s1 in
-  if i = -1 then s
-  else (
-    let s' = String.sub s 0 i in
-    let s'' = String.sub s (i+sublen) (len-i-sublen) in
-    s' ^ s2 ^ replace_str s'' s1 s2
-  )
+Compute the SHA256 hash (using the C<sha256sum> program).
 
-let rec string_split sep str =
-  let len = String.length str in
-  let seplen = String.length sep in
-  let i = find str sep in
-  if i = -1 then [str]
-  else (
-    let s' = String.sub str 0 i in
-    let s'' = String.sub str (i+seplen) (len-i-seplen) in
-    s' :: string_split sep s''
-  )
+=item C<sha384>
 
-let rec find_map f = function
-  | [] -> raise Not_found
-  | x :: xs ->
-      match f x with
-      | Some y -> y
-      | None -> find_map f xs
+Compute the SHA384 hash (using the C<sha384sum> program).
 
-let iteri f xs =
-  let rec loop i = function
-    | [] -> ()
-    | x :: xs -> f i x; loop (i+1) xs
-  in
-  loop 0 xs
+=item C<sha512>
 
-let mapi f xs =
-  let rec loop i = function
-    | [] -> []
-    | x :: xs -> let r = f i x in r :: loop (i+1) xs
-  in
-  loop 0 xs
+Compute the SHA512 hash (using the C<sha512sum> program).
 
-let name_of_argt = function
-  | String n | OptString n | StringList n | Bool n | Int n
-  | FileIn n | FileOut n -> n
+=back
 
-let seq_of_test = function
-  | TestRun s | TestOutput (s, _) | TestOutputList (s, _)
-  | TestOutputInt (s, _) | TestOutputTrue s | TestOutputFalse s
-  | TestOutputLength (s, _) | TestOutputStruct (s, _)
-  | TestLastFail s -> s
+The checksum is returned as a printable string.");
 
-(* Check function names etc. for consistency. *)
-let check_functions () =
-  let contains_uppercase str =
-    let len = String.length str in
-    let rec loop i =
-      if i >= len then false
-      else (
-       let c = str.[i] in
-       if c >= 'A' && c <= 'Z' then true
-       else loop (i+1)
-      )
-    in
-    loop 0
-  in
+  ("tar_in", (RErr, [FileIn "tarfile"; String "directory"]), 69, [],
+   [InitBasicFS, Always, TestOutput (
+      [["tar_in"; "../images/helloworld.tar"; "/"];
+       ["cat"; "/hello"]], "hello\n")],
+   "unpack tarfile to directory",
+   "\
+This command uploads and unpacks local file C<tarfile> (an
+I<uncompressed> tar file) into C<directory>.
 
-  (* Check function names. *)
-  List.iter (
-    fun (name, _, _, _, _, _, _) ->
-      if String.length name >= 7 && String.sub name 0 7 = "guestfs" then
-       failwithf "function name %s does not need 'guestfs' prefix" name;
-      if contains_uppercase name then
-       failwithf "function name %s should not contain uppercase chars" name;
-      if String.contains name '-' then
-       failwithf "function name %s should not contain '-', use '_' instead."
-         name
-  ) all_functions;
+To upload a compressed tarball, use C<guestfs_tgz_in>.");
 
-  (* Check function parameter/return names. *)
-  List.iter (
-    fun (name, style, _, _, _, _, _) ->
-      let check_arg_ret_name n =
-       if contains_uppercase n then
-         failwithf "%s param/ret %s should not contain uppercase chars"
-           name n;
-       if String.contains n '-' || String.contains n '_' then
-         failwithf "%s param/ret %s should not contain '-' or '_'"
-           name n;
-       if n = "value" then
-         failwithf "%s has a param/ret called 'value', which causes conflicts in the OCaml bindings, use something like 'val' or a more descriptive name" n;
-       if n = "argv" || n = "args" then
-         failwithf "%s has a param/ret called 'argv' or 'args', which will cause some conflicts in the generated code" n
-      in
+  ("tar_out", (RErr, [String "directory"; FileOut "tarfile"]), 70, [],
+   [],
+   "pack directory into tarfile",
+   "\
+This command packs the contents of C<directory> and downloads
+it to local file C<tarfile>.
 
-      (match fst style with
-       | RErr -> ()
-       | RInt n | RInt64 n | RBool n | RConstString n | RString n
-       | RStringList n | RPVList n | RVGList n | RLVList n
-       | RStat n | RStatVFS n
-       | RHashtable n ->
-          check_arg_ret_name n
-       | RIntBool (n,m) ->
-          check_arg_ret_name n;
-          check_arg_ret_name m
-      );
-      List.iter (fun arg -> check_arg_ret_name (name_of_argt arg)) (snd style)
-  ) all_functions;
+To download a compressed tarball, use C<guestfs_tgz_out>.");
 
-  (* Check short descriptions. *)
-  List.iter (
-    fun (name, _, _, _, _, shortdesc, _) ->
-      if shortdesc.[0] <> Char.lowercase shortdesc.[0] then
-       failwithf "short description of %s should begin with lowercase." name;
-      let c = shortdesc.[String.length shortdesc-1] in
-      if c = '\n' || c = '.' then
-       failwithf "short description of %s should not end with . or \\n." name
-  ) all_functions;
+  ("tgz_in", (RErr, [FileIn "tarball"; String "directory"]), 71, [],
+   [InitBasicFS, Always, TestOutput (
+      [["tgz_in"; "../images/helloworld.tar.gz"; "/"];
+       ["cat"; "/hello"]], "hello\n")],
+   "unpack compressed tarball to directory",
+   "\
+This command uploads and unpacks local file C<tarball> (a
+I<gzip compressed> tar file) into C<directory>.
 
-  (* Check long dscriptions. *)
-  List.iter (
-    fun (name, _, _, _, _, _, longdesc) ->
-      if longdesc.[String.length longdesc-1] = '\n' then
-       failwithf "long description of %s should not end with \\n." name
-  ) all_functions;
+To upload an uncompressed tarball, use C<guestfs_tar_in>.");
 
-  (* Check proc_nrs. *)
-  List.iter (
-    fun (name, _, proc_nr, _, _, _, _) ->
-      if proc_nr <= 0 then
-       failwithf "daemon function %s should have proc_nr > 0" name
-  ) daemon_functions;
+  ("tgz_out", (RErr, [String "directory"; FileOut "tarball"]), 72, [],
+   [],
+   "pack directory into compressed tarball",
+   "\
+This command packs the contents of C<directory> and downloads
+it to local file C<tarball>.
+
+To download an uncompressed tarball, use C<guestfs_tar_out>.");
+
+  ("mount_ro", (RErr, [String "device"; String "mountpoint"]), 73, [],
+   [InitBasicFS, Always, TestLastFail (
+      [["umount"; "/"];
+       ["mount_ro"; "/dev/sda1"; "/"];
+       ["touch"; "/new"]]);
+    InitBasicFS, Always, TestOutput (
+      [["write_file"; "/new"; "data"; "0"];
+       ["umount"; "/"];
+       ["mount_ro"; "/dev/sda1"; "/"];
+       ["cat"; "/new"]], "data")],
+   "mount a guest disk, read-only",
+   "\
+This is the same as the C<guestfs_mount> command, but it
+mounts the filesystem with the read-only (I<-o ro>) flag.");
 
-  List.iter (
-    fun (name, _, proc_nr, _, _, _, _) ->
-      if proc_nr <> -1 then
-       failwithf "non-daemon function %s should have proc_nr -1" name
-  ) non_daemon_functions;
+  ("mount_options", (RErr, [String "options"; String "device"; String "mountpoint"]), 74, [],
+   [],
+   "mount a guest disk with mount options",
+   "\
+This is the same as the C<guestfs_mount> command, but it
+allows you to set the mount options as for the
+L<mount(8)> I<-o> flag.");
 
-  let proc_nrs =
-    List.map (fun (name, _, proc_nr, _, _, _, _) -> name, proc_nr)
-      daemon_functions in
-  let proc_nrs =
-    List.sort (fun (_,nr1) (_,nr2) -> compare nr1 nr2) proc_nrs in
-  let rec loop = function
-    | [] -> ()
-    | [_] -> ()
-    | (name1,nr1) :: ((name2,nr2) :: _ as rest) when nr1 < nr2 ->
-       loop rest
-    | (name1,nr1) :: (name2,nr2) :: _ ->
-       failwithf "%s and %s have conflicting procedure numbers (%d, %d)"
-         name1 name2 nr1 nr2
-  in
-  loop proc_nrs;
+  ("mount_vfs", (RErr, [String "options"; String "vfstype"; String "device"; String "mountpoint"]), 75, [],
+   [],
+   "mount a guest disk with mount options and vfstype",
+   "\
+This is the same as the C<guestfs_mount> command, but it
+allows you to set both the mount options and the vfstype
+as for the L<mount(8)> I<-o> and I<-t> flags.");
 
-  (* Check tests. *)
-  List.iter (
-    function
-      (* Ignore functions that have no tests.  We generate a
-       * warning when the user does 'make check' instead.
-       *)
-    | name, _, _, _, [], _, _ -> ()
-    | name, _, _, _, tests, _, _ ->
-       let funcs =
-         List.map (
-           fun (_, test) ->
-             match seq_of_test test with
-             | [] ->
-                 failwithf "%s has a test containing an empty sequence" name
-             | cmds -> List.map List.hd cmds
-         ) tests in
-       let funcs = List.flatten funcs in
+  ("debug", (RString "result", [String "subcmd"; StringList "extraargs"]), 76, [],
+   [],
+   "debugging and internals",
+   "\
+The C<guestfs_debug> command exposes some internals of
+C<guestfsd> (the guestfs daemon) that runs inside the
+qemu subprocess.
 
-       let tested = List.mem name funcs in
+There is no comprehensive help for this command.  You have
+to look at the file C<daemon/debug.c> in the libguestfs source
+to find out what you can do.");
 
-       if not tested then
-         failwithf "function %s has tests but does not test itself" name
-  ) all_functions
+  ("lvremove", (RErr, [String "device"]), 77, [],
+   [InitEmpty, Always, TestOutputList (
+      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
+       ["pvcreate"; "/dev/sda1"];
+       ["vgcreate"; "VG"; "/dev/sda1"];
+       ["lvcreate"; "LV1"; "VG"; "50"];
+       ["lvcreate"; "LV2"; "VG"; "50"];
+       ["lvremove"; "/dev/VG/LV1"];
+       ["lvs"]], ["/dev/VG/LV2"]);
+    InitEmpty, Always, TestOutputList (
+      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
+       ["pvcreate"; "/dev/sda1"];
+       ["vgcreate"; "VG"; "/dev/sda1"];
+       ["lvcreate"; "LV1"; "VG"; "50"];
+       ["lvcreate"; "LV2"; "VG"; "50"];
+       ["lvremove"; "/dev/VG"];
+       ["lvs"]], []);
+    InitEmpty, Always, TestOutputList (
+      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
+       ["pvcreate"; "/dev/sda1"];
+       ["vgcreate"; "VG"; "/dev/sda1"];
+       ["lvcreate"; "LV1"; "VG"; "50"];
+       ["lvcreate"; "LV2"; "VG"; "50"];
+       ["lvremove"; "/dev/VG"];
+       ["vgs"]], ["VG"])],
+   "remove an LVM logical volume",
+   "\
+Remove an LVM logical volume C<device>, where C<device> is
+the path to the LV, such as C</dev/VG/LV>.
 
-(* 'pr' prints to the current output file. *)
-let chan = ref stdout
-let pr fs = ksprintf (output_string !chan) fs
+You can also remove all LVs in a volume group by specifying
+the VG name, C</dev/VG>.");
 
-(* Generate a header block in a number of standard styles. *)
-type comment_style = CStyle | HashStyle | OCamlStyle
-type license = GPLv2 | LGPLv2
+  ("vgremove", (RErr, [String "vgname"]), 78, [],
+   [InitEmpty, Always, TestOutputList (
+      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
+       ["pvcreate"; "/dev/sda1"];
+       ["vgcreate"; "VG"; "/dev/sda1"];
+       ["lvcreate"; "LV1"; "VG"; "50"];
+       ["lvcreate"; "LV2"; "VG"; "50"];
+       ["vgremove"; "VG"];
+       ["lvs"]], []);
+    InitEmpty, Always, TestOutputList (
+      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
+       ["pvcreate"; "/dev/sda1"];
+       ["vgcreate"; "VG"; "/dev/sda1"];
+       ["lvcreate"; "LV1"; "VG"; "50"];
+       ["lvcreate"; "LV2"; "VG"; "50"];
+       ["vgremove"; "VG"];
+       ["vgs"]], [])],
+   "remove an LVM volume group",
+   "\
+Remove an LVM volume group C<vgname>, (for example C<VG>).
 
-let generate_header comment license =
-  let c = match comment with
-    | CStyle ->     pr "/* "; " *"
-    | HashStyle ->  pr "# ";  "#"
-    | OCamlStyle -> pr "(* "; " *" in
-  pr "libguestfs generated file\n";
-  pr "%s WARNING: THIS FILE IS GENERATED BY 'src/generator.ml'.\n" c;
-  pr "%s ANY CHANGES YOU MAKE TO THIS FILE WILL BE LOST.\n" c;
-  pr "%s\n" c;
-  pr "%s Copyright (C) 2009 Red Hat Inc.\n" c;
-  pr "%s\n" c;
-  (match license with
-   | GPLv2 ->
-       pr "%s This program is free software; you can redistribute it and/or modify\n" c;
-       pr "%s it under the terms of the GNU General Public License as published by\n" c;
-       pr "%s the Free Software Foundation; either version 2 of the License, or\n" c;
-       pr "%s (at your option) any later version.\n" c;
-       pr "%s\n" c;
-       pr "%s This program is distributed in the hope that it will be useful,\n" c;
-       pr "%s but WITHOUT ANY WARRANTY; without even the implied warranty of\n" c;
-       pr "%s MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n" c;
-       pr "%s GNU General Public License for more details.\n" c;
-       pr "%s\n" c;
-       pr "%s You should have received a copy of the GNU General Public License along\n" c;
-       pr "%s with this program; if not, write to the Free Software Foundation, Inc.,\n" c;
-       pr "%s 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.\n" c;
+This also forcibly removes all logical volumes in the volume
+group (if any).");
 
-   | LGPLv2 ->
-       pr "%s This library is free software; you can redistribute it and/or\n" c;
-       pr "%s modify it under the terms of the GNU Lesser General Public\n" c;
-       pr "%s License as published by the Free Software Foundation; either\n" c;
-       pr "%s version 2 of the License, or (at your option) any later version.\n" c;
-       pr "%s\n" c;
-       pr "%s This library is distributed in the hope that it will be useful,\n" c;
-       pr "%s but WITHOUT ANY WARRANTY; without even the implied warranty of\n" c;
-       pr "%s MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n" c;
-       pr "%s Lesser General Public License for more details.\n" c;
-       pr "%s\n" c;
-       pr "%s You should have received a copy of the GNU Lesser General Public\n" c;
-       pr "%s License along with this library; if not, write to the Free Software\n" c;
-       pr "%s Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA\n" c;
-  );
-  (match comment with
-   | CStyle -> pr " */\n"
-   | HashStyle -> ()
-   | OCamlStyle -> pr " *)\n"
-  );
-  pr "\n"
+  ("pvremove", (RErr, [String "device"]), 79, [],
+   [InitEmpty, Always, TestOutputListOfDevices (
+      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
+       ["pvcreate"; "/dev/sda1"];
+       ["vgcreate"; "VG"; "/dev/sda1"];
+       ["lvcreate"; "LV1"; "VG"; "50"];
+       ["lvcreate"; "LV2"; "VG"; "50"];
+       ["vgremove"; "VG"];
+       ["pvremove"; "/dev/sda1"];
+       ["lvs"]], []);
+    InitEmpty, Always, TestOutputListOfDevices (
+      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
+       ["pvcreate"; "/dev/sda1"];
+       ["vgcreate"; "VG"; "/dev/sda1"];
+       ["lvcreate"; "LV1"; "VG"; "50"];
+       ["lvcreate"; "LV2"; "VG"; "50"];
+       ["vgremove"; "VG"];
+       ["pvremove"; "/dev/sda1"];
+       ["vgs"]], []);
+    InitEmpty, Always, TestOutputListOfDevices (
+      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
+       ["pvcreate"; "/dev/sda1"];
+       ["vgcreate"; "VG"; "/dev/sda1"];
+       ["lvcreate"; "LV1"; "VG"; "50"];
+       ["lvcreate"; "LV2"; "VG"; "50"];
+       ["vgremove"; "VG"];
+       ["pvremove"; "/dev/sda1"];
+       ["pvs"]], [])],
+   "remove an LVM physical volume",
+   "\
+This wipes a physical volume C<device> so that LVM will no longer
+recognise it.
+
+The implementation uses the C<pvremove> command which refuses to
+wipe physical volumes that contain any volume groups, so you have
+to remove those first.");
+
+  ("set_e2label", (RErr, [String "device"; String "label"]), 80, [],
+   [InitBasicFS, Always, TestOutput (
+      [["set_e2label"; "/dev/sda1"; "testlabel"];
+       ["get_e2label"; "/dev/sda1"]], "testlabel")],
+   "set the ext2/3/4 filesystem label",
+   "\
+This sets the ext2/3/4 filesystem label of the filesystem on
+C<device> to C<label>.  Filesystem labels are limited to
+16 characters.
 
-(* Start of main code generation functions below this line. *)
+You can use either C<guestfs_tune2fs_l> or C<guestfs_get_e2label>
+to return the existing label on a filesystem.");
 
-(* Generate the pod documentation for the C API. *)
-let rec generate_actions_pod () =
-  List.iter (
-    fun (shortname, style, _, flags, _, _, longdesc) ->
-      let name = "guestfs_" ^ shortname in
-      pr "=head2 %s\n\n" name;
-      pr " ";
-      generate_prototype ~extern:false ~handle:"handle" name style;
-      pr "\n\n";
-      pr "%s\n\n" longdesc;
-      (match fst style with
-       | RErr ->
-          pr "This function returns 0 on success or -1 on error.\n\n"
-       | RInt _ ->
-          pr "On error this function returns -1.\n\n"
-       | RInt64 _ ->
-          pr "On error this function returns -1.\n\n"
-       | RBool _ ->
-          pr "This function returns a C truth value on success or -1 on error.\n\n"
-       | RConstString _ ->
-          pr "This function returns a string, or NULL on error.
-The string is owned by the guest handle and must I<not> be freed.\n\n"
-       | RString _ ->
-          pr "This function returns a string, or NULL on error.
-I<The caller must free the returned string after use>.\n\n"
-       | RStringList _ ->
-          pr "This function returns a NULL-terminated array of strings
-(like L<environ(3)>), or NULL if there was an error.
-I<The caller must free the strings and the array after use>.\n\n"
-       | RIntBool _ ->
-          pr "This function returns a C<struct guestfs_int_bool *>,
-or NULL if there was an error.
-I<The caller must call C<guestfs_free_int_bool> after use>.\n\n"
-       | RPVList _ ->
-          pr "This function returns a C<struct guestfs_lvm_pv_list *>
-(see E<lt>guestfs-structs.hE<gt>),
-or NULL if there was an error.
-I<The caller must call C<guestfs_free_lvm_pv_list> after use>.\n\n"
-       | RVGList _ ->
-          pr "This function returns a C<struct guestfs_lvm_vg_list *>
-(see E<lt>guestfs-structs.hE<gt>),
-or NULL if there was an error.
-I<The caller must call C<guestfs_free_lvm_vg_list> after use>.\n\n"
-       | RLVList _ ->
-          pr "This function returns a C<struct guestfs_lvm_lv_list *>
-(see E<lt>guestfs-structs.hE<gt>),
-or NULL if there was an error.
-I<The caller must call C<guestfs_free_lvm_lv_list> after use>.\n\n"
-       | RStat _ ->
-          pr "This function returns a C<struct guestfs_stat *>
-(see L<stat(2)> and E<lt>guestfs-structs.hE<gt>),
-or NULL if there was an error.
-I<The caller must call C<free> after use>.\n\n"
-       | RStatVFS _ ->
-          pr "This function returns a C<struct guestfs_statvfs *>
-(see L<statvfs(2)> and E<lt>guestfs-structs.hE<gt>),
-or NULL if there was an error.
-I<The caller must call C<free> after use>.\n\n"
-       | RHashtable _ ->
-          pr "This function returns a NULL-terminated array of
-strings, or NULL if there was an error.
-The array of strings will always have length C<2n+1>, where
-C<n> keys and values alternate, followed by the trailing NULL entry.
-I<The caller must free the strings and the array after use>.\n\n"
-      );
-      if List.mem ProtocolLimitWarning flags then
-       pr "%s\n\n" protocol_limit_warning;
-      if List.mem DangerWillRobinson flags then
-       pr "%s\n\n" danger_will_robinson;
-  ) all_functions_sorted
-
-and generate_structs_pod () =
-  (* LVM structs documentation. *)
-  List.iter (
-    fun (typ, cols) ->
-      pr "=head2 guestfs_lvm_%s\n" typ;
-      pr "\n";
-      pr " struct guestfs_lvm_%s {\n" typ;
-      List.iter (
-       function
-       | name, `String -> pr "  char *%s;\n" name
-       | name, `UUID ->
-           pr "  /* The next field is NOT nul-terminated, be careful when printing it: */\n";
-           pr "  char %s[32];\n" name
-       | name, `Bytes -> pr "  uint64_t %s;\n" name
-       | name, `Int -> pr "  int64_t %s;\n" name
-       | name, `OptPercent ->
-           pr "  /* The next field is [0..100] or -1 meaning 'not present': */\n";
-           pr "  float %s;\n" name
-      ) cols;
-      pr " \n";
-      pr " struct guestfs_lvm_%s_list {\n" typ;
-      pr "   uint32_t len; /* Number of elements in list. */\n";
-      pr "   struct guestfs_lvm_%s *val; /* Elements. */\n" typ;
-      pr " };\n";
-      pr " \n";
-      pr " void guestfs_free_lvm_%s_list (struct guestfs_free_lvm_%s_list *);\n"
-       typ typ;
-      pr "\n"
-  ) ["pv", pv_cols; "vg", vg_cols; "lv", lv_cols]
+  ("get_e2label", (RString "label", [String "device"]), 81, [],
+   [],
+   "get the ext2/3/4 filesystem label",
+   "\
+This returns the ext2/3/4 filesystem label of the filesystem on
+C<device>.");
+
+  ("set_e2uuid", (RErr, [String "device"; String "uuid"]), 82, [],
+   [InitBasicFS, Always, TestOutput (
+      [["set_e2uuid"; "/dev/sda1"; "a3a61220-882b-4f61-89f4-cf24dcc7297d"];
+       ["get_e2uuid"; "/dev/sda1"]], "a3a61220-882b-4f61-89f4-cf24dcc7297d");
+    InitBasicFS, Always, TestOutput (
+      [["set_e2uuid"; "/dev/sda1"; "clear"];
+       ["get_e2uuid"; "/dev/sda1"]], "");
+    (* We can't predict what UUIDs will be, so just check the commands run. *)
+    InitBasicFS, Always, TestRun (
+      [["set_e2uuid"; "/dev/sda1"; "random"]]);
+    InitBasicFS, Always, TestRun (
+      [["set_e2uuid"; "/dev/sda1"; "time"]])],
+   "set the ext2/3/4 filesystem UUID",
+   "\
+This sets the ext2/3/4 filesystem UUID of the filesystem on
+C<device> to C<uuid>.  The format of the UUID and alternatives
+such as C<clear>, C<random> and C<time> are described in the
+L<tune2fs(8)> manpage.
 
-(* Generate the protocol (XDR) file, 'guestfs_protocol.x' and
- * indirectly 'guestfs_protocol.h' and 'guestfs_protocol.c'.
- *
- * We have to use an underscore instead of a dash because otherwise
- * rpcgen generates incorrect code.
- *
- * This header is NOT exported to clients, but see also generate_structs_h.
- *)
-and generate_xdr () =
-  generate_header CStyle LGPLv2;
+You can use either C<guestfs_tune2fs_l> or C<guestfs_get_e2uuid>
+to return the existing UUID of a filesystem.");
 
-  (* This has to be defined to get around a limitation in Sun's rpcgen. *)
-  pr "typedef string str<>;\n";
-  pr "\n";
+  ("get_e2uuid", (RString "uuid", [String "device"]), 83, [],
+   [],
+   "get the ext2/3/4 filesystem UUID",
+   "\
+This returns the ext2/3/4 filesystem UUID of the filesystem on
+C<device>.");
+
+  ("fsck", (RInt "status", [String "fstype"; String "device"]), 84, [],
+   [InitBasicFS, Always, TestOutputInt (
+      [["umount"; "/dev/sda1"];
+       ["fsck"; "ext2"; "/dev/sda1"]], 0);
+    InitBasicFS, Always, TestOutputInt (
+      [["umount"; "/dev/sda1"];
+       ["zero"; "/dev/sda1"];
+       ["fsck"; "ext2"; "/dev/sda1"]], 8)],
+   "run the filesystem checker",
+   "\
+This runs the filesystem checker (fsck) on C<device> which
+should have filesystem type C<fstype>.
 
-  (* LVM internal structures. *)
-  List.iter (
-    function
-    | typ, cols ->
-       pr "struct guestfs_lvm_int_%s {\n" typ;
-       List.iter (function
-                  | name, `String -> pr "  string %s<>;\n" name
-                  | name, `UUID -> pr "  opaque %s[32];\n" name
-                  | name, `Bytes -> pr "  hyper %s;\n" name
-                  | name, `Int -> pr "  hyper %s;\n" name
-                  | name, `OptPercent -> pr "  float %s;\n" name
-                 ) cols;
-       pr "};\n";
-       pr "\n";
-       pr "typedef struct guestfs_lvm_int_%s guestfs_lvm_int_%s_list<>;\n" typ typ;
-       pr "\n";
-  ) ["pv", pv_cols; "vg", vg_cols; "lv", lv_cols];
+The returned integer is the status.  See L<fsck(8)> for the
+list of status codes from C<fsck>.
 
-  (* Stat internal structures. *)
-  List.iter (
-    function
-    | typ, cols ->
-       pr "struct guestfs_int_%s {\n" typ;
-       List.iter (function
-                  | name, `Int -> pr "  hyper %s;\n" name
-                 ) cols;
-       pr "};\n";
-       pr "\n";
-  ) ["stat", stat_cols; "statvfs", statvfs_cols];
+Notes:
 
-  List.iter (
-    fun (shortname, style, _, _, _, _, _) ->
-      let name = "guestfs_" ^ shortname in
+=over 4
 
-      (match snd style with
-       | [] -> ()
-       | args ->
-          pr "struct %s_args {\n" name;
-          List.iter (
-            function
-            | String n -> pr "  string %s<>;\n" n
-            | OptString n -> pr "  str *%s;\n" n
-            | StringList n -> pr "  str %s<>;\n" n
-            | Bool n -> pr "  bool %s;\n" n
-            | Int n -> pr "  int %s;\n" n
-            | FileIn _ | FileOut _ -> ()
-          ) args;
-          pr "};\n\n"
-      );
-      (match fst style with
-       | RErr -> ()
-       | RInt n ->
-          pr "struct %s_ret {\n" name;
-          pr "  int %s;\n" n;
-          pr "};\n\n"
-       | RInt64 n ->
-          pr "struct %s_ret {\n" name;
-          pr "  hyper %s;\n" n;
-          pr "};\n\n"
-       | RBool n ->
-          pr "struct %s_ret {\n" name;
-          pr "  bool %s;\n" n;
-          pr "};\n\n"
-       | RConstString _ ->
-          failwithf "RConstString cannot be returned from a daemon function"
-       | RString n ->
-          pr "struct %s_ret {\n" name;
-          pr "  string %s<>;\n" n;
-          pr "};\n\n"
-       | RStringList n ->
-          pr "struct %s_ret {\n" name;
-          pr "  str %s<>;\n" n;
-          pr "};\n\n"
-       | RIntBool (n,m) ->
-          pr "struct %s_ret {\n" name;
-          pr "  int %s;\n" n;
-          pr "  bool %s;\n" m;
-          pr "};\n\n"
-       | RPVList n ->
-          pr "struct %s_ret {\n" name;
-          pr "  guestfs_lvm_int_pv_list %s;\n" n;
-          pr "};\n\n"
-       | RVGList n ->
-          pr "struct %s_ret {\n" name;
-          pr "  guestfs_lvm_int_vg_list %s;\n" n;
-          pr "};\n\n"
-       | RLVList n ->
-          pr "struct %s_ret {\n" name;
-          pr "  guestfs_lvm_int_lv_list %s;\n" n;
-          pr "};\n\n"
-       | RStat n ->
-          pr "struct %s_ret {\n" name;
-          pr "  guestfs_int_stat %s;\n" n;
-          pr "};\n\n"
-       | RStatVFS n ->
-          pr "struct %s_ret {\n" name;
-          pr "  guestfs_int_statvfs %s;\n" n;
-          pr "};\n\n"
-       | RHashtable n ->
-          pr "struct %s_ret {\n" name;
-          pr "  str %s<>;\n" n;
-          pr "};\n\n"
-      );
-  ) daemon_functions;
+=item *
 
-  (* Table of procedure numbers. *)
-  pr "enum guestfs_procedure {\n";
-  List.iter (
-    fun (shortname, _, proc_nr, _, _, _, _) ->
-      pr "  GUESTFS_PROC_%s = %d,\n" (String.uppercase shortname) proc_nr
-  ) daemon_functions;
-  pr "  GUESTFS_PROC_NR_PROCS\n";
-  pr "};\n";
-  pr "\n";
+Multiple status codes can be summed together.
 
-  (* Having to choose a maximum message size is annoying for several
-   * reasons (it limits what we can do in the API), but it (a) makes
-   * the protocol a lot simpler, and (b) provides a bound on the size
-   * of the daemon which operates in limited memory space.  For large
-   * file transfers you should use FTP.
-   *)
-  pr "const GUESTFS_MESSAGE_MAX = %d;\n" (4 * 1024 * 1024);
-  pr "\n";
+=item *
 
-  (* Message header, etc. *)
-  pr "\
-const GUESTFS_PROGRAM = 0x2000F5F5;
-const GUESTFS_PROTOCOL_VERSION = 1;
+A non-zero return code can mean \"success\", for example if
+errors have been corrected on the filesystem.
 
-enum guestfs_message_direction {
-  GUESTFS_DIRECTION_CALL = 0,        /* client -> daemon */
-  GUESTFS_DIRECTION_REPLY = 1        /* daemon -> client */
-};
+=item *
 
-enum guestfs_message_status {
-  GUESTFS_STATUS_OK = 0,
-  GUESTFS_STATUS_ERROR = 1
-};
+Checking or repairing NTFS volumes is not supported
+(by linux-ntfs).
 
-const GUESTFS_ERROR_LEN = 256;
+=back
 
-struct guestfs_message_error {
-  string error_message<GUESTFS_ERROR_LEN>;
-};
+This command is entirely equivalent to running C<fsck -a -t fstype device>.");
 
-/* For normal requests and replies (not involving any FileIn or
- * FileOut parameters), the protocol is:
- *
- * For requests:
- *   total length (header + args, but not including length word itself)
- *   header
- *   guestfs_foo_args struct
- * For replies:
- *   total length (as above)
- *   header
- *   guestfs_foo_ret struct
- */
+  ("zero", (RErr, [String "device"]), 85, [],
+   [InitBasicFS, Always, TestOutput (
+      [["umount"; "/dev/sda1"];
+       ["zero"; "/dev/sda1"];
+       ["file"; "/dev/sda1"]], "data")],
+   "write zeroes to the device",
+   "\
+This command writes zeroes over the first few blocks of C<device>.
 
-struct guestfs_message_header {
-  unsigned prog;                     /* GUESTFS_PROGRAM */
-  unsigned vers;                     /* GUESTFS_PROTOCOL_VERSION */
-  guestfs_procedure proc;            /* GUESTFS_PROC_x */
-  guestfs_message_direction direction;
-  unsigned serial;                   /* message serial number */
-  guestfs_message_status status;
-};
+How many blocks are zeroed isn't specified (but it's I<not> enough
+to securely wipe the device).  It should be sufficient to remove
+any partition tables, filesystem superblocks and so on.
 
-/* Chunked encoding used to transfer files, for FileIn and FileOut
- * parameters.
- *
- * For requests which have >= 1 FileIn parameter:
- *   length of header + args (but not length word itself, and not chunks)
- *   header
- *   guestfs_foo_args struct
- *   sequence of chunks for FileIn param #0
- *   sequence of chunks for FileIn param #1 etc
- *
- * For replies which have >= 1 FileOut parameter:
- *   length of header + ret (but not length word itself, and not chunks)
- *   header
- *   guestfs_foo_ret struct
- *   sequence of chunks for FileOut param #0
- *   sequence of chunks for FileOut param #1 etc
- */
-const GUESTFS_MAX_CHUNK_SIZE = 8192;
+See also: C<guestfs_scrub_device>.");
 
-struct guestfs_chunk {
-  int cancel;                       /* if non-zero, transfer is cancelled */
-  /* data size is 0 bytes if the transfer has finished successfully */
-  opaque data<GUESTFS_MAX_CHUNK_SIZE>;
-};
-"
+  ("grub_install", (RErr, [String "root"; String "device"]), 86, [],
+   [InitBasicFS, Always, TestOutputTrue (
+      [["grub_install"; "/"; "/dev/sda1"];
+       ["is_dir"; "/boot"]])],
+   "install GRUB",
+   "\
+This command installs GRUB (the Grand Unified Bootloader) on
+C<device>, with the root directory being C<root>.");
+
+  ("cp", (RErr, [String "src"; String "dest"]), 87, [],
+   [InitBasicFS, Always, TestOutput (
+      [["write_file"; "/old"; "file content"; "0"];
+       ["cp"; "/old"; "/new"];
+       ["cat"; "/new"]], "file content");
+    InitBasicFS, Always, TestOutputTrue (
+      [["write_file"; "/old"; "file content"; "0"];
+       ["cp"; "/old"; "/new"];
+       ["is_file"; "/old"]]);
+    InitBasicFS, Always, TestOutput (
+      [["write_file"; "/old"; "file content"; "0"];
+       ["mkdir"; "/dir"];
+       ["cp"; "/old"; "/dir/new"];
+       ["cat"; "/dir/new"]], "file content")],
+   "copy a file",
+   "\
+This copies a file from C<src> to C<dest> where C<dest> is
+either a destination filename or destination directory.");
+
+  ("cp_a", (RErr, [String "src"; String "dest"]), 88, [],
+   [InitBasicFS, Always, TestOutput (
+      [["mkdir"; "/olddir"];
+       ["mkdir"; "/newdir"];
+       ["write_file"; "/olddir/file"; "file content"; "0"];
+       ["cp_a"; "/olddir"; "/newdir"];
+       ["cat"; "/newdir/olddir/file"]], "file content")],
+   "copy a file or directory recursively",
+   "\
+This copies a file or directory from C<src> to C<dest>
+recursively using the C<cp -a> command.");
+
+  ("mv", (RErr, [String "src"; String "dest"]), 89, [],
+   [InitBasicFS, Always, TestOutput (
+      [["write_file"; "/old"; "file content"; "0"];
+       ["mv"; "/old"; "/new"];
+       ["cat"; "/new"]], "file content");
+    InitBasicFS, Always, TestOutputFalse (
+      [["write_file"; "/old"; "file content"; "0"];
+       ["mv"; "/old"; "/new"];
+       ["is_file"; "/old"]])],
+   "move a file",
+   "\
+This moves a file from C<src> to C<dest> where C<dest> is
+either a destination filename or destination directory.");
 
-(* Generate the guestfs-structs.h file. *)
-and generate_structs_h () =
-  generate_header CStyle LGPLv2;
+  ("drop_caches", (RErr, [Int "whattodrop"]), 90, [],
+   [InitEmpty, Always, TestRun (
+      [["drop_caches"; "3"]])],
+   "drop kernel page cache, dentries and inodes",
+   "\
+This instructs the guest kernel to drop its page cache,
+and/or dentries and inode caches.  The parameter C<whattodrop>
+tells the kernel what precisely to drop, see
+L<http://linux-mm.org/Drop_Caches>
 
-  (* This is a public exported header file containing various
-   * structures.  The structures are carefully written to have
-   * exactly the same in-memory format as the XDR structures that
-   * we use on the wire to the daemon.  The reason for creating
-   * copies of these structures here is just so we don't have to
-   * export the whole of guestfs_protocol.h (which includes much
-   * unrelated and XDR-dependent stuff that we don't want to be
-   * public, or required by clients).
-   *
-   * To reiterate, we will pass these structures to and from the
-   * client with a simple assignment or memcpy, so the format
-   * must be identical to what rpcgen / the RFC defines.
-   *)
+Setting C<whattodrop> to 3 should drop everything.
 
-  (* guestfs_int_bool structure. *)
-  pr "struct guestfs_int_bool {\n";
-  pr "  int32_t i;\n";
-  pr "  int32_t b;\n";
-  pr "};\n";
-  pr "\n";
+This automatically calls L<sync(2)> before the operation,
+so that the maximum guest memory is freed.");
 
-  (* LVM public structures. *)
-  List.iter (
-    function
-    | typ, cols ->
-       pr "struct guestfs_lvm_%s {\n" typ;
-       List.iter (
-         function
-         | name, `String -> pr "  char *%s;\n" name
-         | name, `UUID -> pr "  char %s[32]; /* this is NOT nul-terminated, be careful when printing */\n" name
-         | name, `Bytes -> pr "  uint64_t %s;\n" name
-         | name, `Int -> pr "  int64_t %s;\n" name
-         | name, `OptPercent -> pr "  float %s; /* [0..100] or -1 */\n" name
-       ) cols;
-       pr "};\n";
-       pr "\n";
-       pr "struct guestfs_lvm_%s_list {\n" typ;
-       pr "  uint32_t len;\n";
-       pr "  struct guestfs_lvm_%s *val;\n" typ;
-       pr "};\n";
-       pr "\n"
-  ) ["pv", pv_cols; "vg", vg_cols; "lv", lv_cols];
+  ("dmesg", (RString "kmsgs", []), 91, [],
+   [InitEmpty, Always, TestRun (
+      [["dmesg"]])],
+   "return kernel messages",
+   "\
+This returns the kernel messages (C<dmesg> output) from
+the guest kernel.  This is sometimes useful for extended
+debugging of problems.
+
+Another way to get the same information is to enable
+verbose messages with C<guestfs_set_verbose> or by setting
+the environment variable C<LIBGUESTFS_DEBUG=1> before
+running the program.");
+
+  ("ping_daemon", (RErr, []), 92, [],
+   [InitEmpty, Always, TestRun (
+      [["ping_daemon"]])],
+   "ping the guest daemon",
+   "\
+This is a test probe into the guestfs daemon running inside
+the qemu subprocess.  Calling this function checks that the
+daemon responds to the ping message, without affecting the daemon
+or attached block device(s) in any other way.");
+
+  ("equal", (RBool "equality", [String "file1"; String "file2"]), 93, [],
+   [InitBasicFS, Always, TestOutputTrue (
+      [["write_file"; "/file1"; "contents of a file"; "0"];
+       ["cp"; "/file1"; "/file2"];
+       ["equal"; "/file1"; "/file2"]]);
+    InitBasicFS, Always, TestOutputFalse (
+      [["write_file"; "/file1"; "contents of a file"; "0"];
+       ["write_file"; "/file2"; "contents of another file"; "0"];
+       ["equal"; "/file1"; "/file2"]]);
+    InitBasicFS, Always, TestLastFail (
+      [["equal"; "/file1"; "/file2"]])],
+   "test if two files have equal contents",
+   "\
+This compares the two files C<file1> and C<file2> and returns
+true if their content is exactly equal, or false otherwise.
 
-  (* Stat structures. *)
-  List.iter (
-    function
-    | typ, cols ->
-       pr "struct guestfs_%s {\n" typ;
-       List.iter (
-         function
-         | name, `Int -> pr "  int64_t %s;\n" name
-       ) cols;
-       pr "};\n";
-       pr "\n"
-  ) ["stat", stat_cols; "statvfs", statvfs_cols]
+The external L<cmp(1)> program is used for the comparison.");
 
-(* Generate the guestfs-actions.h file. *)
-and generate_actions_h () =
-  generate_header CStyle LGPLv2;
-  List.iter (
-    fun (shortname, style, _, _, _, _, _) ->
-      let name = "guestfs_" ^ shortname in
-      generate_prototype ~single_line:true ~newline:true ~handle:"handle"
-       name style
-  ) all_functions
+  ("strings", (RStringList "stringsout", [String "path"]), 94, [ProtocolLimitWarning],
+   [InitBasicFS, Always, TestOutputList (
+      [["write_file"; "/new"; "hello\nworld\n"; "0"];
+       ["strings"; "/new"]], ["hello"; "world"]);
+    InitBasicFS, Always, TestOutputList (
+      [["touch"; "/new"];
+       ["strings"; "/new"]], [])],
+   "print the printable strings in a file",
+   "\
+This runs the L<strings(1)> command on a file and returns
+the list of printable strings found.");
+
+  ("strings_e", (RStringList "stringsout", [String "encoding"; String "path"]), 95, [ProtocolLimitWarning],
+   [InitBasicFS, Always, TestOutputList (
+      [["write_file"; "/new"; "hello\nworld\n"; "0"];
+       ["strings_e"; "b"; "/new"]], []);
+    InitBasicFS, Disabled, TestOutputList (
+      [["write_file"; "/new"; "\000h\000e\000l\000l\000o\000\n\000w\000o\000r\000l\000d\000\n"; "24"];
+       ["strings_e"; "b"; "/new"]], ["hello"; "world"])],
+   "print the printable strings in a file",
+   "\
+This is like the C<guestfs_strings> command, but allows you to
+specify the encoding.
 
-(* Generate the client-side dispatch stubs. *)
-and generate_client_actions () =
-  generate_header CStyle LGPLv2;
+See the L<strings(1)> manpage for the full list of encodings.
 
-  pr "\
-#include <stdio.h>
-#include <stdlib.h>
+Commonly useful encodings are C<l> (lower case L) which will
+show strings inside Windows/x86 files.
 
-#include \"guestfs.h\"
-#include \"guestfs_protocol.h\"
+The returned strings are transcoded to UTF-8.");
 
-#define error guestfs_error
-#define perrorf guestfs_perrorf
-#define safe_malloc guestfs_safe_malloc
-#define safe_realloc guestfs_safe_realloc
-#define safe_strdup guestfs_safe_strdup
-#define safe_memdup guestfs_safe_memdup
+  ("hexdump", (RString "dump", [String "path"]), 96, [ProtocolLimitWarning],
+   [InitBasicFS, Always, TestOutput (
+      [["write_file"; "/new"; "hello\nworld\n"; "12"];
+       ["hexdump"; "/new"]], "00000000  68 65 6c 6c 6f 0a 77 6f  72 6c 64 0a              |hello.world.|\n0000000c\n");
+    (* Test for RHBZ#501888c2 regression which caused large hexdump
+     * commands to segfault.
+     *)
+    InitBasicFS, Always, TestRun (
+      [["mount_vfs"; "ro"; "squashfs"; "/dev/sdd"; "/"];
+       ["hexdump"; "/100krandom"]])],
+   "dump a file in hexadecimal",
+   "\
+This runs C<hexdump -C> on the given C<path>.  The result is
+the human-readable, canonical hex dump of the file.");
 
-/* Check the return message from a call for validity. */
-static int
-check_reply_header (guestfs_h *g,
-                    const struct guestfs_message_header *hdr,
-                    int proc_nr, int serial)
-{
-  if (hdr->prog != GUESTFS_PROGRAM) {
-    error (g, \"wrong program (%%d/%%d)\", hdr->prog, GUESTFS_PROGRAM);
-    return -1;
-  }
-  if (hdr->vers != GUESTFS_PROTOCOL_VERSION) {
-    error (g, \"wrong protocol version (%%d/%%d)\",
-          hdr->vers, GUESTFS_PROTOCOL_VERSION);
-    return -1;
-  }
-  if (hdr->direction != GUESTFS_DIRECTION_REPLY) {
-    error (g, \"unexpected message direction (%%d/%%d)\",
-          hdr->direction, GUESTFS_DIRECTION_REPLY);
-    return -1;
-  }
-  if (hdr->proc != proc_nr) {
-    error (g, \"unexpected procedure number (%%d/%%d)\", hdr->proc, proc_nr);
-    return -1;
-  }
-  if (hdr->serial != serial) {
-    error (g, \"unexpected serial (%%d/%%d)\", hdr->serial, serial);
-    return -1;
-  }
+  ("zerofree", (RErr, [String "device"]), 97, [],
+   [InitNone, Always, TestOutput (
+      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
+       ["mkfs"; "ext3"; "/dev/sda1"];
+       ["mount"; "/dev/sda1"; "/"];
+       ["write_file"; "/new"; "test file"; "0"];
+       ["umount"; "/dev/sda1"];
+       ["zerofree"; "/dev/sda1"];
+       ["mount"; "/dev/sda1"; "/"];
+       ["cat"; "/new"]], "test file")],
+   "zero unused inodes and disk blocks on ext2/3 filesystem",
+   "\
+This runs the I<zerofree> program on C<device>.  This program
+claims to zero unused inodes and disk blocks on an ext2/3
+filesystem, thus making it possible to compress the filesystem
+more effectively.
 
-  return 0;
-}
+You should B<not> run this program if the filesystem is
+mounted.
 
-/* Check we are in the right state to run a high-level action. */
-static int
-check_state (guestfs_h *g, const char *caller)
-{
-  if (!guestfs_is_ready (g)) {
-    if (guestfs_is_config (g))
-      error (g, \"%%s: call launch() before using this function\",
-        caller);
-    else if (guestfs_is_launching (g))
-      error (g, \"%%s: call wait_ready() before using this function\",
-        caller);
-    else
-      error (g, \"%%s called from the wrong state, %%d != READY\",
-        caller, guestfs_get_state (g));
-    return -1;
-  }
-  return 0;
-}
+It is possible that using this program can damage the filesystem
+or data on the filesystem.");
 
-";
+  ("pvresize", (RErr, [String "device"]), 98, [],
+   [],
+   "resize an LVM physical volume",
+   "\
+This resizes (expands or shrinks) an existing LVM physical
+volume to match the new size of the underlying device.");
 
-  (* Client-side stubs for each function. *)
-  List.iter (
-    fun (shortname, style, _, _, _, _, _) ->
-      let name = "guestfs_" ^ shortname in
+  ("sfdisk_N", (RErr, [String "device"; Int "partnum";
+                      Int "cyls"; Int "heads"; Int "sectors";
+                      String "line"]), 99, [DangerWillRobinson],
+   [],
+   "modify a single partition on a block device",
+   "\
+This runs L<sfdisk(8)> option to modify just the single
+partition C<n> (note: C<n> counts from 1).
 
-      (* Generate the state struct which stores the high-level
-       * state between callback functions.
-       *)
-      pr "struct %s_state {\n" shortname;
-      pr "  int cb_state;\n";
-      pr "  struct guestfs_message_header hdr;\n";
-      pr "  struct guestfs_message_error err;\n";
-      (match fst style with
-       | RErr -> ()
-       | RConstString _ ->
-          failwithf "RConstString cannot be returned from a daemon function"
-       | RInt _ | RInt64 _
-       | RBool _ | RString _ | RStringList _
-       | RIntBool _
-       | RPVList _ | RVGList _ | RLVList _
-       | RStat _ | RStatVFS _
-       | RHashtable _ ->
-          pr "  struct %s_ret ret;\n" name
-      );
-      pr "};\n";
-      pr "\n";
+For other parameters, see C<guestfs_sfdisk>.  You should usually
+pass C<0> for the cyls/heads/sectors parameters.");
 
-      (* Generate the callback function. *)
-      pr "static void %s_cb (guestfs_h *g, void *data, XDR *xdr)\n" shortname;
-      pr "{\n";
-      pr "  guestfs_main_loop *ml = guestfs_get_main_loop (g);\n";
-      pr "  struct %s_state *state = (struct %s_state *) data;\n" shortname shortname;
-      pr "\n";
-      pr "  if (!xdr_guestfs_message_header (xdr, &state->hdr)) {\n";
-      pr "    error (g, \"%s: failed to parse reply header\");\n" name;
-      pr "    return;\n";
-      pr "  }\n";
-      pr "  if (state->hdr.status == GUESTFS_STATUS_ERROR) {\n";
-      pr "    if (!xdr_guestfs_message_error (xdr, &state->err)) {\n";
-      pr "      error (g, \"%s: failed to parse reply error\");\n" name;
-      pr "      return;\n";
-      pr "    }\n";
-      pr "    goto done;\n";
-      pr "  }\n";
+  ("sfdisk_l", (RString "partitions", [String "device"]), 100, [],
+   [],
+   "display the partition table",
+   "\
+This displays the partition table on C<device>, in the
+human-readable output of the L<sfdisk(8)> command.  It is
+not intended to be parsed.");
 
-      (match fst style with
-       | RErr -> ()
-       | RConstString _ ->
-          failwithf "RConstString cannot be returned from a daemon function"
-       | RInt _ | RInt64 _
-       | RBool _ | RString _ | RStringList _
-       | RIntBool _
-       | RPVList _ | RVGList _ | RLVList _
-       | RStat _ | RStatVFS _
-       | RHashtable _ ->
-           pr "  if (!xdr_%s_ret (xdr, &state->ret)) {\n" name;
-           pr "    error (g, \"%s: failed to parse reply\");\n" name;
-           pr "    return;\n";
-           pr "  }\n";
-      );
+  ("sfdisk_kernel_geometry", (RString "partitions", [String "device"]), 101, [],
+   [],
+   "display the kernel geometry",
+   "\
+This displays the kernel's idea of the geometry of C<device>.
 
-      pr " done:\n";
-      pr "  state->cb_state = 1;\n";
-      pr "  ml->main_loop_quit (ml, g);\n";
-      pr "}\n\n";
+The result is in human-readable format, and not designed to
+be parsed.");
 
-      (* Generate the action stub. *)
-      generate_prototype ~extern:false ~semicolon:false ~newline:true
-       ~handle:"g" name style;
+  ("sfdisk_disk_geometry", (RString "partitions", [String "device"]), 102, [],
+   [],
+   "display the disk geometry from the partition table",
+   "\
+This displays the disk geometry of C<device> read from the
+partition table.  Especially in the case where the underlying
+block device has been resized, this can be different from the
+kernel's idea of the geometry (see C<guestfs_sfdisk_kernel_geometry>).
 
-      let error_code =
-       match fst style with
-       | RErr | RInt _ | RInt64 _ | RBool _ -> "-1"
-       | RConstString _ ->
-           failwithf "RConstString cannot be returned from a daemon function"
-       | RString _ | RStringList _ | RIntBool _
-       | RPVList _ | RVGList _ | RLVList _
-       | RStat _ | RStatVFS _
-       | RHashtable _ ->
-           "NULL" in
+The result is in human-readable format, and not designed to
+be parsed.");
 
-      pr "{\n";
+  ("vg_activate_all", (RErr, [Bool "activate"]), 103, [],
+   [],
+   "activate or deactivate all volume groups",
+   "\
+This command activates or (if C<activate> is false) deactivates
+all logical volumes in all volume groups.
+If activated, then they are made known to the
+kernel, ie. they appear as C</dev/mapper> devices.  If deactivated,
+then those devices disappear.
 
-      (match snd style with
-       | [] -> ()
-       | _ -> pr "  struct %s_args args;\n" name
-      );
+This command is the same as running C<vgchange -a y|n>");
 
-      pr "  struct %s_state state;\n" shortname;
-      pr "  guestfs_main_loop *ml = guestfs_get_main_loop (g);\n";
-      pr "  int serial;\n";
-      pr "\n";
-      pr "  if (check_state (g, \"%s\") == -1) return %s;\n" name error_code;
-      pr "\n";
-      pr "  memset (&state, 0, sizeof state);\n";
-      pr "\n";
+  ("vg_activate", (RErr, [Bool "activate"; StringList "volgroups"]), 104, [],
+   [],
+   "activate or deactivate some volume groups",
+   "\
+This command activates or (if C<activate> is false) deactivates
+all logical volumes in the listed volume groups C<volgroups>.
+If activated, then they are made known to the
+kernel, ie. they appear as C</dev/mapper> devices.  If deactivated,
+then those devices disappear.
+
+This command is the same as running C<vgchange -a y|n volgroups...>
+
+Note that if C<volgroups> is an empty list then B<all> volume groups
+are activated or deactivated.");
+
+  ("lvresize", (RErr, [String "device"; Int "mbytes"]), 105, [],
+   [InitNone, Always, TestOutput (
+    [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
+     ["pvcreate"; "/dev/sda1"];
+     ["vgcreate"; "VG"; "/dev/sda1"];
+     ["lvcreate"; "LV"; "VG"; "10"];
+     ["mkfs"; "ext2"; "/dev/VG/LV"];
+     ["mount"; "/dev/VG/LV"; "/"];
+     ["write_file"; "/new"; "test content"; "0"];
+     ["umount"; "/"];
+     ["lvresize"; "/dev/VG/LV"; "20"];
+     ["e2fsck_f"; "/dev/VG/LV"];
+     ["resize2fs"; "/dev/VG/LV"];
+     ["mount"; "/dev/VG/LV"; "/"];
+     ["cat"; "/new"]], "test content")],
+   "resize an LVM logical volume",
+   "\
+This resizes (expands or shrinks) an existing LVM logical
+volume to C<mbytes>.  When reducing, data in the reduced part
+is lost.");
 
-      (* Dispatch the main header and arguments. *)
-      (match snd style with
-       | [] ->
-          pr "  serial = guestfs_send (g, GUESTFS_PROC_%s, NULL, NULL);\n"
-            (String.uppercase shortname)
-       | args ->
-          List.iter (
-            function
-            | String n ->
-                pr "  args.%s = (char *) %s;\n" n n
-            | OptString n ->
-                pr "  args.%s = %s ? (char **) &%s : NULL;\n" n n n
-            | StringList n ->
-                pr "  args.%s.%s_val = (char **) %s;\n" n n n;
-                pr "  for (args.%s.%s_len = 0; %s[args.%s.%s_len]; args.%s.%s_len++) ;\n" n n n n n n n;
-            | Bool n ->
-                pr "  args.%s = %s;\n" n n
-            | Int n ->
-                pr "  args.%s = %s;\n" n n
-            | FileIn _ | FileOut _ -> ()
-          ) args;
-          pr "  serial = guestfs_send (g, GUESTFS_PROC_%s,\n"
-            (String.uppercase shortname);
-          pr "                     (xdrproc_t) xdr_%s_args, (char *) &args);\n"
-            name;
-      );
-      pr "  if (serial == -1)\n";
-      pr "    return %s;\n" error_code;
-      pr "\n";
+  ("resize2fs", (RErr, [String "device"]), 106, [],
+   [], (* lvresize tests this *)
+   "resize an ext2/ext3 filesystem",
+   "\
+This resizes an ext2 or ext3 filesystem to match the size of
+the underlying device.
+
+I<Note:> It is sometimes required that you run C<guestfs_e2fsck_f>
+on the C<device> before calling this command.  For unknown reasons
+C<resize2fs> sometimes gives an error about this and sometimes not.
+In any case, it is always safe to call C<guestfs_e2fsck_f> before
+calling this function.");
+
+  ("find", (RStringList "names", [String "directory"]), 107, [],
+   [InitBasicFS, Always, TestOutputList (
+      [["find"; "/"]], ["lost+found"]);
+    InitBasicFS, Always, TestOutputList (
+      [["touch"; "/a"];
+       ["mkdir"; "/b"];
+       ["touch"; "/b/c"];
+       ["find"; "/"]], ["a"; "b"; "b/c"; "lost+found"]);
+    InitBasicFS, Always, TestOutputList (
+      [["mkdir_p"; "/a/b/c"];
+       ["touch"; "/a/b/c/d"];
+       ["find"; "/a/b/"]], ["c"; "c/d"])],
+   "find all files and directories",
+   "\
+This command lists out all files and directories, recursively,
+starting at C<directory>.  It is essentially equivalent to
+running the shell command C<find directory -print> but some
+post-processing happens on the output, described below.
 
-      (* Send any additional files requested. *)
-      List.iter (
-       function
-       | FileIn n ->
-           pr "  if (send_file (g, %s) == -1)\n" n;
-           pr "    return %s;\n" error_code;
-           pr "\n";
-       | _ -> ()
-      ) (snd style);
+This returns a list of strings I<without any prefix>.  Thus
+if the directory structure was:
 
-      (* Wait for the reply from the remote end. *)
-      pr "  state.cb_state = 0;\n";
-      pr "  guestfs_set_reply_callback (g, %s_cb, &state);\n" shortname;
-      pr "  (void) ml->main_loop_run (ml, g);\n";
-      pr "  guestfs_set_reply_callback (g, NULL, NULL);\n";
-      pr "  if (!state.cb_state) {\n";
-      pr "    error (g, \"%s failed, see earlier error messages\");\n" name;
-      pr "    return %s;\n" error_code;
-      pr "  }\n";
-      pr "\n";
+ /tmp/a
+ /tmp/b
+ /tmp/c/d
 
-      pr "  if (check_reply_header (g, &state.hdr, GUESTFS_PROC_%s, serial) == -1)\n"
-       (String.uppercase shortname);
-      pr "    return %s;\n" error_code;
-      pr "\n";
+then the returned list from C<guestfs_find> C</tmp> would be
+4 elements:
 
-      pr "  if (state.hdr.status == GUESTFS_STATUS_ERROR) {\n";
-      pr "    error (g, \"%%s\", state.err.error_message);\n";
-      pr "    return %s;\n" error_code;
-      pr "  }\n";
-      pr "\n";
+ a
+ b
+ c
+ c/d
 
-      (* Expecting to receive further files (FileOut)? *)
-      List.iter (
-       function
-       | FileOut n ->
-           pr "  if (receive_file (g, %s) == -1)\n" n;
-           pr "    return %s;\n" error_code;
-           pr "\n";
-       | _ -> ()
-      ) (snd style);
+If C<directory> is not a directory, then this command returns
+an error.
 
-      (match fst style with
-       | RErr -> pr "  return 0;\n"
-       | RInt n | RInt64 n | RBool n ->
-          pr "  return state.ret.%s;\n" n
-       | RConstString _ ->
-          failwithf "RConstString cannot be returned from a daemon function"
-       | RString n ->
-          pr "  return state.ret.%s; /* caller will free */\n" n
-       | RStringList n | RHashtable n ->
-          pr "  /* caller will free this, but we need to add a NULL entry */\n";
-          pr "  state.ret.%s.%s_val =\n" n n;
-          pr "    safe_realloc (g, state.ret.%s.%s_val,\n" n n;
-          pr "                  sizeof (char *) * (state.ret.%s.%s_len + 1));\n"
-            n n;
-          pr "  state.ret.%s.%s_val[state.ret.%s.%s_len] = NULL;\n" n n n n;
-          pr "  return state.ret.%s.%s_val;\n" n n
-       | RIntBool _ ->
-          pr "  /* caller with free this */\n";
-          pr "  return safe_memdup (g, &state.ret, sizeof (state.ret));\n"
-       | RPVList n | RVGList n | RLVList n
-       | RStat n | RStatVFS n ->
-          pr "  /* caller will free this */\n";
-          pr "  return safe_memdup (g, &state.ret.%s, sizeof (state.ret.%s));\n" n n
-      );
+The returned list is sorted.");
 
-      pr "}\n\n"
-  ) daemon_functions
+  ("e2fsck_f", (RErr, [String "device"]), 108, [],
+   [], (* lvresize tests this *)
+   "check an ext2/ext3 filesystem",
+   "\
+This runs C<e2fsck -p -f device>, ie. runs the ext2/ext3
+filesystem checker on C<device>, noninteractively (C<-p>),
+even if the filesystem appears to be clean (C<-f>).
 
-(* Generate daemon/actions.h. *)
-and generate_daemon_actions_h () =
-  generate_header CStyle GPLv2;
+This command is only needed because of C<guestfs_resize2fs>
+(q.v.).  Normally you should use C<guestfs_fsck>.");
 
-  pr "#include \"../src/guestfs_protocol.h\"\n";
-  pr "\n";
+  ("sleep", (RErr, [Int "secs"]), 109, [],
+   [InitNone, Always, TestRun (
+    [["sleep"; "1"]])],
+   "sleep for some seconds",
+   "\
+Sleep for C<secs> seconds.");
 
-  List.iter (
-    fun (name, style, _, _, _, _, _) ->
-       generate_prototype
-         ~single_line:true ~newline:true ~in_daemon:true ~prefix:"do_"
-         name style;
-  ) daemon_functions
+  ("ntfs_3g_probe", (RInt "status", [Bool "rw"; String "device"]), 110, [],
+   [InitNone, Always, TestOutputInt (
+      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
+       ["mkfs"; "ntfs"; "/dev/sda1"];
+       ["ntfs_3g_probe"; "true"; "/dev/sda1"]], 0);
+    InitNone, Always, TestOutputInt (
+      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
+       ["mkfs"; "ext2"; "/dev/sda1"];
+       ["ntfs_3g_probe"; "true"; "/dev/sda1"]], 12)],
+   "probe NTFS volume",
+   "\
+This command runs the L<ntfs-3g.probe(8)> command which probes
+an NTFS C<device> for mountability.  (Not all NTFS volumes can
+be mounted read-write, and some cannot be mounted at all).
 
-(* Generate the server-side stubs. *)
-and generate_daemon_actions () =
-  generate_header CStyle GPLv2;
+C<rw> is a boolean flag.  Set it to true if you want to test
+if the volume can be mounted read-write.  Set it to false if
+you want to test if the volume can be mounted read-only.
 
-  pr "#define _GNU_SOURCE // for strchrnul\n";
-  pr "\n";
-  pr "#include <stdio.h>\n";
-  pr "#include <stdlib.h>\n";
-  pr "#include <string.h>\n";
-  pr "#include <inttypes.h>\n";
-  pr "#include <ctype.h>\n";
-  pr "#include <rpc/types.h>\n";
-  pr "#include <rpc/xdr.h>\n";
-  pr "\n";
-  pr "#include \"daemon.h\"\n";
-  pr "#include \"../src/guestfs_protocol.h\"\n";
-  pr "#include \"actions.h\"\n";
-  pr "\n";
+The return value is an integer which C<0> if the operation
+would succeed, or some non-zero value documented in the
+L<ntfs-3g.probe(8)> manual page.");
 
-  List.iter (
-    fun (name, style, _, _, _, _, _) ->
-      (* Generate server-side stubs. *)
-      pr "static void %s_stub (XDR *xdr_in)\n" name;
-      pr "{\n";
-      let error_code =
-       match fst style with
-       | RErr | RInt _ -> pr "  int r;\n"; "-1"
-       | RInt64 _ -> pr "  int64_t r;\n"; "-1"
-       | RBool _ -> pr "  int r;\n"; "-1"
-       | RConstString _ ->
-           failwithf "RConstString cannot be returned from a daemon function"
-       | RString _ -> pr "  char *r;\n"; "NULL"
-       | RStringList _ | RHashtable _ -> pr "  char **r;\n"; "NULL"
-       | RIntBool _ -> pr "  guestfs_%s_ret *r;\n" name; "NULL"
-       | RPVList _ -> pr "  guestfs_lvm_int_pv_list *r;\n"; "NULL"
-       | RVGList _ -> pr "  guestfs_lvm_int_vg_list *r;\n"; "NULL"
-       | RLVList _ -> pr "  guestfs_lvm_int_lv_list *r;\n"; "NULL"
-       | RStat _ -> pr "  guestfs_int_stat *r;\n"; "NULL"
-       | RStatVFS _ -> pr "  guestfs_int_statvfs *r;\n"; "NULL" in
+  ("sh", (RString "output", [String "command"]), 111, [],
+   [], (* XXX needs tests *)
+   "run a command via the shell",
+   "\
+This call runs a command from the guest filesystem via the
+guest's C</bin/sh>.
 
-      (match snd style with
-       | [] -> ()
-       | args ->
-          pr "  struct guestfs_%s_args args;\n" name;
-          List.iter (
-            function
-            | String n
-            | OptString n -> pr "  const char *%s;\n" n
-            | StringList n -> pr "  char **%s;\n" n
-            | Bool n -> pr "  int %s;\n" n
-            | Int n -> pr "  int %s;\n" n
-            | FileIn _ | FileOut _ -> ()
-          ) args
-      );
-      pr "\n";
+This is like C<guestfs_command>, but passes the command to:
 
-      (match snd style with
-       | [] -> ()
-       | args ->
-          pr "  memset (&args, 0, sizeof args);\n";
-          pr "\n";
-          pr "  if (!xdr_guestfs_%s_args (xdr_in, &args)) {\n" name;
-          pr "    reply_with_error (\"%%s: daemon failed to decode procedure arguments\", \"%s\");\n" name;
-          pr "    return;\n";
-          pr "  }\n";
-          List.iter (
-            function
-            | String n -> pr "  %s = args.%s;\n" n n
-            | OptString n -> pr "  %s = args.%s ? *args.%s : NULL;\n" n n n
-            | StringList n ->
-                pr "  args.%s.%s_val = realloc (args.%s.%s_val, sizeof (char *) * (args.%s.%s_len+1));\n" n n n n n n;
-                pr "  args.%s.%s_val[args.%s.%s_len] = NULL;\n" n n n n;
-                pr "  %s = args.%s.%s_val;\n" n n n
-            | Bool n -> pr "  %s = args.%s;\n" n n
-            | Int n -> pr "  %s = args.%s;\n" n n
-            | FileIn _ | FileOut _ -> ()
-          ) args;
-          pr "\n"
-      );
+ /bin/sh -c \"command\"
 
-      (* Don't want to call the impl with any FileIn or FileOut
-       * parameters, since these go "outside" the RPC protocol.
-       *)
-      let argsnofile =
-       List.filter (function FileIn _ | FileOut _ -> false | _ -> true)
-         (snd style) in
-      pr "  r = do_%s " name;
-      generate_call_args argsnofile;
-      pr ";\n";
+Depending on the guest's shell, this usually results in
+wildcards being expanded, shell expressions being interpolated
+and so on.
 
-      pr "  if (r == %s)\n" error_code;
-      pr "    /* do_%s has already called reply_with_error */\n" name;
-      pr "    goto done;\n";
-      pr "\n";
+All the provisos about C<guestfs_command> apply to this call.");
 
-      (* If there are any FileOut parameters, then the impl must
-       * send its own reply.
-       *)
-      let no_reply =
-       List.exists (function FileOut _ -> true | _ -> false) (snd style) in
-      if no_reply then
-       pr "  /* do_%s has already sent a reply */\n" name
-      else (
-       match fst style with
-       | RErr -> pr "  reply (NULL, NULL);\n"
-       | RInt n | RInt64 n | RBool n ->
-           pr "  struct guestfs_%s_ret ret;\n" name;
-           pr "  ret.%s = r;\n" n;
-           pr "  reply ((xdrproc_t) &xdr_guestfs_%s_ret, (char *) &ret);\n"
-             name
-       | RConstString _ ->
-           failwithf "RConstString cannot be returned from a daemon function"
-       | RString n ->
-           pr "  struct guestfs_%s_ret ret;\n" name;
-           pr "  ret.%s = r;\n" n;
-           pr "  reply ((xdrproc_t) &xdr_guestfs_%s_ret, (char *) &ret);\n"
-             name;
-           pr "  free (r);\n"
-       | RStringList n | RHashtable n ->
-           pr "  struct guestfs_%s_ret ret;\n" name;
-           pr "  ret.%s.%s_len = count_strings (r);\n" n n;
-           pr "  ret.%s.%s_val = r;\n" n n;
-           pr "  reply ((xdrproc_t) &xdr_guestfs_%s_ret, (char *) &ret);\n"
-             name;
-           pr "  free_strings (r);\n"
-       | RIntBool _ ->
-           pr "  reply ((xdrproc_t) xdr_guestfs_%s_ret, (char *) r);\n"
-             name;
-           pr "  xdr_free ((xdrproc_t) xdr_guestfs_%s_ret, (char *) r);\n" name
-       | RPVList n | RVGList n | RLVList n
-       | RStat n | RStatVFS n ->
-           pr "  struct guestfs_%s_ret ret;\n" name;
-           pr "  ret.%s = *r;\n" n;
-           pr "  reply ((xdrproc_t) xdr_guestfs_%s_ret, (char *) &ret);\n"
-             name;
-           pr "  xdr_free ((xdrproc_t) xdr_guestfs_%s_ret, (char *) &ret);\n"
-             name
-      );
+  ("sh_lines", (RStringList "lines", [String "command"]), 112, [],
+   [], (* XXX needs tests *)
+   "run a command via the shell returning lines",
+   "\
+This is the same as C<guestfs_sh>, but splits the result
+into a list of lines.
+
+See also: C<guestfs_command_lines>");
+
+  ("glob_expand", (RStringList "paths", [String "pattern"]), 113, [],
+   [InitBasicFS, Always, TestOutputList (
+      [["mkdir_p"; "/a/b/c"];
+       ["touch"; "/a/b/c/d"];
+       ["touch"; "/a/b/c/e"];
+       ["glob_expand"; "/a/b/c/*"]], ["/a/b/c/d"; "/a/b/c/e"]);
+    InitBasicFS, Always, TestOutputList (
+      [["mkdir_p"; "/a/b/c"];
+       ["touch"; "/a/b/c/d"];
+       ["touch"; "/a/b/c/e"];
+       ["glob_expand"; "/a/*/c/*"]], ["/a/b/c/d"; "/a/b/c/e"]);
+    InitBasicFS, Always, TestOutputList (
+      [["mkdir_p"; "/a/b/c"];
+       ["touch"; "/a/b/c/d"];
+       ["touch"; "/a/b/c/e"];
+       ["glob_expand"; "/a/*/x/*"]], [])],
+   "expand a wildcard path",
+   "\
+This command searches for all the pathnames matching
+C<pattern> according to the wildcard expansion rules
+used by the shell.
 
-      (* Free the args. *)
-      (match snd style with
-       | [] ->
-          pr "done: ;\n";
-       | _ ->
-          pr "done:\n";
-          pr "  xdr_free ((xdrproc_t) xdr_guestfs_%s_args, (char *) &args);\n"
-            name
-      );
+If no paths match, then this returns an empty list
+(note: not an error).
 
-      pr "}\n\n";
-  ) daemon_functions;
+It is just a wrapper around the C L<glob(3)> function
+with flags C<GLOB_MARK|GLOB_BRACE>.
+See that manual page for more details.");
 
-  (* Dispatch function. *)
-  pr "void dispatch_incoming_message (XDR *xdr_in)\n";
-  pr "{\n";
-  pr "  switch (proc_nr) {\n";
+  ("scrub_device", (RErr, [String "device"]), 114, [DangerWillRobinson],
+   [InitNone, Always, TestRun (        (* use /dev/sdc because it's smaller *)
+      [["scrub_device"; "/dev/sdc"]])],
+   "scrub (securely wipe) a device",
+   "\
+This command writes patterns over C<device> to make data retrieval
+more difficult.
 
-  List.iter (
-    fun (name, style, _, _, _, _, _) ->
-       pr "    case GUESTFS_PROC_%s:\n" (String.uppercase name);
-       pr "      %s_stub (xdr_in);\n" name;
-       pr "      break;\n"
-  ) daemon_functions;
+It is an interface to the L<scrub(1)> program.  See that
+manual page for more details.");
 
-  pr "    default:\n";
-  pr "      reply_with_error (\"dispatch_incoming_message: unknown procedure number %%d\", proc_nr);\n";
-  pr "  }\n";
-  pr "}\n";
-  pr "\n";
+  ("scrub_file", (RErr, [String "file"]), 115, [],
+   [InitBasicFS, Always, TestRun (
+      [["write_file"; "/file"; "content"; "0"];
+       ["scrub_file"; "/file"]])],
+   "scrub (securely wipe) a file",
+   "\
+This command writes patterns over a file to make data retrieval
+more difficult.
 
-  (* LVM columns and tokenization functions. *)
-  (* XXX This generates crap code.  We should rethink how we
-   * do this parsing.
-   *)
-  List.iter (
-    function
-    | typ, cols ->
-       pr "static const char *lvm_%s_cols = \"%s\";\n"
-         typ (String.concat "," (List.map fst cols));
-       pr "\n";
+The file is I<removed> after scrubbing.
 
-       pr "static int lvm_tokenize_%s (char *str, struct guestfs_lvm_int_%s *r)\n" typ typ;
-       pr "{\n";
-       pr "  char *tok, *p, *next;\n";
-       pr "  int i, j;\n";
-       pr "\n";
-       (*
-       pr "  fprintf (stderr, \"%%s: <<%%s>>\\n\", __func__, str);\n";
-       pr "\n";
-       *)
-       pr "  if (!str) {\n";
-       pr "    fprintf (stderr, \"%%s: failed: passed a NULL string\\n\", __func__);\n";
-       pr "    return -1;\n";
-       pr "  }\n";
-       pr "  if (!*str || isspace (*str)) {\n";
-       pr "    fprintf (stderr, \"%%s: failed: passed a empty string or one beginning with whitespace\\n\", __func__);\n";
-       pr "    return -1;\n";
-       pr "  }\n";
-       pr "  tok = str;\n";
-       List.iter (
-         fun (name, coltype) ->
-           pr "  if (!tok) {\n";
-           pr "    fprintf (stderr, \"%%s: failed: string finished early, around token %%s\\n\", __func__, \"%s\");\n" name;
-           pr "    return -1;\n";
-           pr "  }\n";
-           pr "  p = strchrnul (tok, ',');\n";
-           pr "  if (*p) next = p+1; else next = NULL;\n";
-           pr "  *p = '\\0';\n";
-           (match coltype with
-            | `String ->
-                pr "  r->%s = strdup (tok);\n" name;
-                pr "  if (r->%s == NULL) {\n" name;
-                pr "    perror (\"strdup\");\n";
-                pr "    return -1;\n";
-                pr "  }\n"
-            | `UUID ->
-                pr "  for (i = j = 0; i < 32; ++j) {\n";
-                pr "    if (tok[j] == '\\0') {\n";
-                pr "      fprintf (stderr, \"%%s: failed to parse UUID from '%%s'\\n\", __func__, tok);\n";
-                pr "      return -1;\n";
-                pr "    } else if (tok[j] != '-')\n";
-                pr "      r->%s[i++] = tok[j];\n" name;
-                pr "  }\n";
-            | `Bytes ->
-                pr "  if (sscanf (tok, \"%%\"SCNu64, &r->%s) != 1) {\n" name;
-                pr "    fprintf (stderr, \"%%s: failed to parse size '%%s' from token %%s\\n\", __func__, tok, \"%s\");\n" name;
-                pr "    return -1;\n";
-                pr "  }\n";
-            | `Int ->
-                pr "  if (sscanf (tok, \"%%\"SCNi64, &r->%s) != 1) {\n" name;
-                pr "    fprintf (stderr, \"%%s: failed to parse int '%%s' from token %%s\\n\", __func__, tok, \"%s\");\n" name;
-                pr "    return -1;\n";
-                pr "  }\n";
-            | `OptPercent ->
-                pr "  if (tok[0] == '\\0')\n";
-                pr "    r->%s = -1;\n" name;
-                pr "  else if (sscanf (tok, \"%%f\", &r->%s) != 1) {\n" name;
-                pr "    fprintf (stderr, \"%%s: failed to parse float '%%s' from token %%s\\n\", __func__, tok, \"%s\");\n" name;
-                pr "    return -1;\n";
-                pr "  }\n";
-           );
-           pr "  tok = next;\n";
-       ) cols;
-
-       pr "  if (tok != NULL) {\n";
-       pr "    fprintf (stderr, \"%%s: failed: extra tokens at end of string\\n\", __func__);\n";
-       pr "    return -1;\n";
-       pr "  }\n";
-       pr "  return 0;\n";
-       pr "}\n";
-       pr "\n";
+It is an interface to the L<scrub(1)> program.  See that
+manual page for more details.");
 
-       pr "guestfs_lvm_int_%s_list *\n" typ;
-       pr "parse_command_line_%ss (void)\n" typ;
-       pr "{\n";
-       pr "  char *out, *err;\n";
-       pr "  char *p, *pend;\n";
-       pr "  int r, i;\n";
-       pr "  guestfs_lvm_int_%s_list *ret;\n" typ;
-       pr "  void *newp;\n";
-       pr "\n";
-       pr "  ret = malloc (sizeof *ret);\n";
-       pr "  if (!ret) {\n";
-       pr "    reply_with_perror (\"malloc\");\n";
-       pr "    return NULL;\n";
-       pr "  }\n";
-       pr "\n";
-       pr "  ret->guestfs_lvm_int_%s_list_len = 0;\n" typ;
-       pr "  ret->guestfs_lvm_int_%s_list_val = NULL;\n" typ;
-       pr "\n";
-       pr "  r = command (&out, &err,\n";
-       pr "           \"/sbin/lvm\", \"%ss\",\n" typ;
-       pr "           \"-o\", lvm_%s_cols, \"--unbuffered\", \"--noheadings\",\n" typ;
-       pr "           \"--nosuffix\", \"--separator\", \",\", \"--units\", \"b\", NULL);\n";
-       pr "  if (r == -1) {\n";
-       pr "    reply_with_error (\"%%s\", err);\n";
-       pr "    free (out);\n";
-       pr "    free (err);\n";
-       pr "    free (ret);\n";
-       pr "    return NULL;\n";
-       pr "  }\n";
-       pr "\n";
-       pr "  free (err);\n";
-       pr "\n";
-       pr "  /* Tokenize each line of the output. */\n";
-       pr "  p = out;\n";
-       pr "  i = 0;\n";
-       pr "  while (p) {\n";
-       pr "    pend = strchr (p, '\\n');       /* Get the next line of output. */\n";
-       pr "    if (pend) {\n";
-       pr "      *pend = '\\0';\n";
-       pr "      pend++;\n";
-       pr "    }\n";
-       pr "\n";
-       pr "    while (*p && isspace (*p))      /* Skip any leading whitespace. */\n";
-       pr "      p++;\n";
-       pr "\n";
-       pr "    if (!*p) {                      /* Empty line?  Skip it. */\n";
-       pr "      p = pend;\n";
-       pr "      continue;\n";
-       pr "    }\n";
-       pr "\n";
-       pr "    /* Allocate some space to store this next entry. */\n";
-       pr "    newp = realloc (ret->guestfs_lvm_int_%s_list_val,\n" typ;
-       pr "                sizeof (guestfs_lvm_int_%s) * (i+1));\n" typ;
-       pr "    if (newp == NULL) {\n";
-       pr "      reply_with_perror (\"realloc\");\n";
-       pr "      free (ret->guestfs_lvm_int_%s_list_val);\n" typ;
-       pr "      free (ret);\n";
-       pr "      free (out);\n";
-       pr "      return NULL;\n";
-       pr "    }\n";
-       pr "    ret->guestfs_lvm_int_%s_list_val = newp;\n" typ;
-       pr "\n";
-       pr "    /* Tokenize the next entry. */\n";
-       pr "    r = lvm_tokenize_%s (p, &ret->guestfs_lvm_int_%s_list_val[i]);\n" typ typ;
-       pr "    if (r == -1) {\n";
-       pr "      reply_with_error (\"failed to parse output of '%ss' command\");\n" typ;
-        pr "      free (ret->guestfs_lvm_int_%s_list_val);\n" typ;
-        pr "      free (ret);\n";
-       pr "      free (out);\n";
-       pr "      return NULL;\n";
-       pr "    }\n";
-       pr "\n";
-       pr "    ++i;\n";
-       pr "    p = pend;\n";
-       pr "  }\n";
-       pr "\n";
-       pr "  ret->guestfs_lvm_int_%s_list_len = i;\n" typ;
-       pr "\n";
-       pr "  free (out);\n";
-       pr "  return ret;\n";
-       pr "}\n"
+  ("scrub_freespace", (RErr, [String "dir"]), 116, [],
+   [], (* XXX needs testing *)
+   "scrub (securely wipe) free space",
+   "\
+This command creates the directory C<dir> and then fills it
+with files until the filesystem is full, and scrubs the files
+as for C<guestfs_scrub_file>, and deletes them.
+The intention is to scrub any free space on the partition
+containing C<dir>.
+
+It is an interface to the L<scrub(1)> program.  See that
+manual page for more details.");
+
+  ("mkdtemp", (RString "dir", [String "template"]), 117, [],
+   [InitBasicFS, Always, TestRun (
+      [["mkdir"; "/tmp"];
+       ["mkdtemp"; "/tmp/tmpXXXXXX"]])],
+   "create a temporary directory",
+   "\
+This command creates a temporary directory.  The
+C<template> parameter should be a full pathname for the
+temporary directory name with the final six characters being
+\"XXXXXX\".
 
-  ) ["pv", pv_cols; "vg", vg_cols; "lv", lv_cols]
+For example: \"/tmp/myprogXXXXXX\" or \"/Temp/myprogXXXXXX\",
+the second one being suitable for Windows filesystems.
 
-(* Generate the tests. *)
-and generate_tests () =
-  generate_header CStyle GPLv2;
+The name of the temporary directory that was created
+is returned.
 
-  pr "\
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <unistd.h>
-#include <sys/types.h>
-#include <fcntl.h>
+The temporary directory is created with mode 0700
+and is owned by root.
 
-#include \"guestfs.h\"
+The caller is responsible for deleting the temporary
+directory and its contents after use.
 
-static guestfs_h *g;
-static int suppress_error = 0;
+See also: L<mkdtemp(3)>");
 
-static void print_error (guestfs_h *g, void *data, const char *msg)
-{
-  if (!suppress_error)
-    fprintf (stderr, \"%%s\\n\", msg);
-}
+  ("wc_l", (RInt "lines", [String "path"]), 118, [],
+   [InitBasicFS, Always, TestOutputInt (
+      [["mount_vfs"; "ro"; "squashfs"; "/dev/sdd"; "/"];
+       ["wc_l"; "/10klines"]], 10000)],
+   "count lines in a file",
+   "\
+This command counts the lines in a file, using the
+C<wc -l> external command.");
+
+  ("wc_w", (RInt "words", [String "path"]), 119, [],
+   [InitBasicFS, Always, TestOutputInt (
+      [["mount_vfs"; "ro"; "squashfs"; "/dev/sdd"; "/"];
+       ["wc_w"; "/10klines"]], 10000)],
+   "count words in a file",
+   "\
+This command counts the words in a file, using the
+C<wc -w> external command.");
+
+  ("wc_c", (RInt "chars", [String "path"]), 120, [],
+   [InitBasicFS, Always, TestOutputInt (
+      [["mount_vfs"; "ro"; "squashfs"; "/dev/sdd"; "/"];
+       ["wc_c"; "/100kallspaces"]], 102400)],
+   "count characters in a file",
+   "\
+This command counts the characters in a file, using the
+C<wc -c> external command.");
 
-static void print_strings (char * const * const argv)
-{
-  int argc;
+]
 
-  for (argc = 0; argv[argc] != NULL; ++argc)
-    printf (\"\\t%%s\\n\", argv[argc]);
-}
+let all_functions = non_daemon_functions @ daemon_functions
 
-/*
-static void print_table (char * const * const argv)
-{
-  int i;
+(* In some places we want the functions to be displayed sorted
+ * alphabetically, so this is useful:
+ *)
+let all_functions_sorted =
+  List.sort (fun (n1,_,_,_,_,_,_) (n2,_,_,_,_,_,_) ->
+              compare n1 n2) all_functions
 
-  for (i = 0; argv[i] != NULL; i += 2)
-    printf (\"%%s: %%s\\n\", argv[i], argv[i+1]);
-}
-*/
+(* Column names and types from LVM PVs/VGs/LVs. *)
+let pv_cols = [
+  "pv_name", `String;
+  "pv_uuid", `UUID;
+  "pv_fmt", `String;
+  "pv_size", `Bytes;
+  "dev_size", `Bytes;
+  "pv_free", `Bytes;
+  "pv_used", `Bytes;
+  "pv_attr", `String (* XXX *);
+  "pv_pe_count", `Int;
+  "pv_pe_alloc_count", `Int;
+  "pv_tags", `String;
+  "pe_start", `Bytes;
+  "pv_mda_count", `Int;
+  "pv_mda_free", `Bytes;
+(* Not in Fedora 10:
+  "pv_mda_size", `Bytes;
+*)
+]
+let vg_cols = [
+  "vg_name", `String;
+  "vg_uuid", `UUID;
+  "vg_fmt", `String;
+  "vg_attr", `String (* XXX *);
+  "vg_size", `Bytes;
+  "vg_free", `Bytes;
+  "vg_sysid", `String;
+  "vg_extent_size", `Bytes;
+  "vg_extent_count", `Int;
+  "vg_free_count", `Int;
+  "max_lv", `Int;
+  "max_pv", `Int;
+  "pv_count", `Int;
+  "lv_count", `Int;
+  "snap_count", `Int;
+  "vg_seqno", `Int;
+  "vg_tags", `String;
+  "vg_mda_count", `Int;
+  "vg_mda_free", `Bytes;
+(* Not in Fedora 10:
+  "vg_mda_size", `Bytes;
+*)
+]
+let lv_cols = [
+  "lv_name", `String;
+  "lv_uuid", `UUID;
+  "lv_attr", `String (* XXX *);
+  "lv_major", `Int;
+  "lv_minor", `Int;
+  "lv_kernel_major", `Int;
+  "lv_kernel_minor", `Int;
+  "lv_size", `Bytes;
+  "seg_count", `Int;
+  "origin", `String;
+  "snap_percent", `OptPercent;
+  "copy_percent", `OptPercent;
+  "move_pv", `String;
+  "lv_tags", `String;
+  "mirror_log", `String;
+  "modules", `String;
+]
 
-static void no_test_warnings (void)
-{
-";
+(* Column names and types from stat structures.
+ * NB. Can't use things like 'st_atime' because glibc header files
+ * define some of these as macros.  Ugh.
+ *)
+let stat_cols = [
+  "dev", `Int;
+  "ino", `Int;
+  "mode", `Int;
+  "nlink", `Int;
+  "uid", `Int;
+  "gid", `Int;
+  "rdev", `Int;
+  "size", `Int;
+  "blksize", `Int;
+  "blocks", `Int;
+  "atime", `Int;
+  "mtime", `Int;
+  "ctime", `Int;
+]
+let statvfs_cols = [
+  "bsize", `Int;
+  "frsize", `Int;
+  "blocks", `Int;
+  "bfree", `Int;
+  "bavail", `Int;
+  "files", `Int;
+  "ffree", `Int;
+  "favail", `Int;
+  "fsid", `Int;
+  "flag", `Int;
+  "namemax", `Int;
+]
 
-  List.iter (
-    function
-    | name, _, _, _, [], _, _ ->
-       pr "  fprintf (stderr, \"warning: \\\"guestfs_%s\\\" has no tests\\n\");\n" name
-    | name, _, _, _, tests, _, _ -> ()
-  ) all_functions;
+(* Used for testing language bindings. *)
+type callt =
+  | CallString of string
+  | CallOptString of string option
+  | CallStringList of string list
+  | CallInt of int
+  | CallBool of bool
 
-  pr "}\n";
-  pr "\n";
+(* Useful functions.
+ * Note we don't want to use any external OCaml libraries which
+ * makes this a bit harder than it should be.
+ *)
+let failwithf fs = ksprintf failwith fs
 
-  (* Generate the actual tests.  Note that we generate the tests
-   * in reverse order, deliberately, so that (in general) the
-   * newest tests run first.  This makes it quicker and easier to
-   * debug them.
-   *)
-  let test_names =
-    List.map (
-      fun (name, _, _, _, tests, _, _) ->
-       mapi (generate_one_test name) tests
-    ) (List.rev all_functions) in
-  let test_names = List.concat test_names in
-  let nr_tests = List.length test_names in
+let replace_char s c1 c2 =
+  let s2 = String.copy s in
+  let r = ref false in
+  for i = 0 to String.length s2 - 1 do
+    if String.unsafe_get s2 i = c1 then (
+      String.unsafe_set s2 i c2;
+      r := true
+    )
+  done;
+  if not !r then s else s2
 
-  pr "\
-int main (int argc, char *argv[])
-{
-  char c = 0;
-  int failed = 0;
-  const char *srcdir;
-  int fd;
-  char buf[256];
-  int nr_tests, test_num = 0;
+let isspace c =
+  c = ' '
+  (* || c = '\f' *) || c = '\n' || c = '\r' || c = '\t' (* || c = '\v' *)
 
-  no_test_warnings ();
+let triml ?(test = isspace) str =
+  let i = ref 0 in
+  let n = ref (String.length str) in
+  while !n > 0 && test str.[!i]; do
+    decr n;
+    incr i
+  done;
+  if !i = 0 then str
+  else String.sub str !i !n
 
-  g = guestfs_create ();
-  if (g == NULL) {
-    printf (\"guestfs_create FAILED\\n\");
-    exit (1);
-  }
+let trimr ?(test = isspace) str =
+  let n = ref (String.length str) in
+  while !n > 0 && test str.[!n-1]; do
+    decr n
+  done;
+  if !n = String.length str then str
+  else String.sub str 0 !n
 
-  guestfs_set_error_handler (g, print_error, NULL);
+let trim ?(test = isspace) str =
+  trimr ~test (triml ~test str)
 
-  srcdir = getenv (\"srcdir\");
-  if (!srcdir) srcdir = \".\";
-  guestfs_set_path (g, srcdir);
+let rec find s sub =
+  let len = String.length s in
+  let sublen = String.length sub in
+  let rec loop i =
+    if i <= len-sublen then (
+      let rec loop2 j =
+       if j < sublen then (
+         if s.[i+j] = sub.[j] then loop2 (j+1)
+         else -1
+       ) else
+         i (* found *)
+      in
+      let r = loop2 0 in
+      if r = -1 then loop (i+1) else r
+    ) else
+      -1 (* not found *)
+  in
+  loop 0
 
-  snprintf (buf, sizeof buf, \"%%s/test1.img\", srcdir);
-  fd = open (buf, O_WRONLY|O_CREAT|O_NOCTTY|O_NONBLOCK|O_TRUNC, 0666);
-  if (fd == -1) {
-    perror (buf);
-    exit (1);
-  }
-  if (lseek (fd, %d, SEEK_SET) == -1) {
-    perror (\"lseek\");
-    close (fd);
-    unlink (buf);
-    exit (1);
-  }
-  if (write (fd, &c, 1) == -1) {
-    perror (\"write\");
-    close (fd);
-    unlink (buf);
-    exit (1);
-  }
-  if (close (fd) == -1) {
-    perror (buf);
-    unlink (buf);
-    exit (1);
-  }
-  if (guestfs_add_drive (g, buf) == -1) {
-    printf (\"guestfs_add_drive %%s FAILED\\n\", buf);
-    exit (1);
-  }
+let rec replace_str s s1 s2 =
+  let len = String.length s in
+  let sublen = String.length s1 in
+  let i = find s s1 in
+  if i = -1 then s
+  else (
+    let s' = String.sub s 0 i in
+    let s'' = String.sub s (i+sublen) (len-i-sublen) in
+    s' ^ s2 ^ replace_str s'' s1 s2
+  )
 
-  snprintf (buf, sizeof buf, \"%%s/test2.img\", srcdir);
-  fd = open (buf, O_WRONLY|O_CREAT|O_NOCTTY|O_NONBLOCK|O_TRUNC, 0666);
-  if (fd == -1) {
-    perror (buf);
-    exit (1);
-  }
-  if (lseek (fd, %d, SEEK_SET) == -1) {
-    perror (\"lseek\");
-    close (fd);
-    unlink (buf);
-    exit (1);
-  }
-  if (write (fd, &c, 1) == -1) {
-    perror (\"write\");
-    close (fd);
-    unlink (buf);
-    exit (1);
-  }
-  if (close (fd) == -1) {
-    perror (buf);
-    unlink (buf);
-    exit (1);
-  }
-  if (guestfs_add_drive (g, buf) == -1) {
-    printf (\"guestfs_add_drive %%s FAILED\\n\", buf);
-    exit (1);
-  }
+let rec string_split sep str =
+  let len = String.length str in
+  let seplen = String.length sep in
+  let i = find str sep in
+  if i = -1 then [str]
+  else (
+    let s' = String.sub str 0 i in
+    let s'' = String.sub str (i+seplen) (len-i-seplen) in
+    s' :: string_split sep s''
+  )
 
-  snprintf (buf, sizeof buf, \"%%s/test3.img\", srcdir);
-  fd = open (buf, O_WRONLY|O_CREAT|O_NOCTTY|O_NONBLOCK|O_TRUNC, 0666);
-  if (fd == -1) {
-    perror (buf);
-    exit (1);
-  }
-  if (lseek (fd, %d, SEEK_SET) == -1) {
-    perror (\"lseek\");
-    close (fd);
-    unlink (buf);
-    exit (1);
-  }
-  if (write (fd, &c, 1) == -1) {
-    perror (\"write\");
-    close (fd);
-    unlink (buf);
-    exit (1);
-  }
-  if (close (fd) == -1) {
-    perror (buf);
-    unlink (buf);
-    exit (1);
-  }
-  if (guestfs_add_drive (g, buf) == -1) {
-    printf (\"guestfs_add_drive %%s FAILED\\n\", buf);
-    exit (1);
-  }
+let files_equal n1 n2 =
+  let cmd = sprintf "cmp -s %s %s" (Filename.quote n1) (Filename.quote n2) in
+  match Sys.command cmd with
+  | 0 -> true
+  | 1 -> false
+  | i -> failwithf "%s: failed with error code %d" cmd i
 
-  if (guestfs_launch (g) == -1) {
-    printf (\"guestfs_launch FAILED\\n\");
-    exit (1);
-  }
-  if (guestfs_wait_ready (g) == -1) {
-    printf (\"guestfs_wait_ready FAILED\\n\");
-    exit (1);
-  }
+let rec find_map f = function
+  | [] -> raise Not_found
+  | x :: xs ->
+      match f x with
+      | Some y -> y
+      | None -> find_map f xs
 
-  nr_tests = %d;
+let iteri f xs =
+  let rec loop i = function
+    | [] -> ()
+    | x :: xs -> f i x; loop (i+1) xs
+  in
+  loop 0 xs
 
-" (500 * 1024 * 1024) (50 * 1024 * 1024) (10 * 1024 * 1024) nr_tests;
+let mapi f xs =
+  let rec loop i = function
+    | [] -> []
+    | x :: xs -> let r = f i x in r :: loop (i+1) xs
+  in
+  loop 0 xs
 
-  iteri (
-    fun i test_name ->
-      pr "  test_num++;\n";
-      pr "  printf (\"%%3d/%%3d %s\\n\", test_num, nr_tests);\n" test_name;
-      pr "  if (%s () == -1) {\n" test_name;
-      pr "    printf (\"%s FAILED\\n\");\n" test_name;
-      pr "    failed++;\n";
-      pr "  }\n";
-  ) test_names;
-  pr "\n";
+let name_of_argt = function
+  | String n | OptString n | StringList n | Bool n | Int n
+  | FileIn n | FileOut n -> n
 
-  pr "  guestfs_close (g);\n";
-  pr "  snprintf (buf, sizeof buf, \"%%s/test1.img\", srcdir);\n";
-  pr "  unlink (buf);\n";
-  pr "  snprintf (buf, sizeof buf, \"%%s/test2.img\", srcdir);\n";
-  pr "  unlink (buf);\n";
-  pr "  snprintf (buf, sizeof buf, \"%%s/test3.img\", srcdir);\n";
-  pr "  unlink (buf);\n";
-  pr "\n";
+let seq_of_test = function
+  | TestRun s | TestOutput (s, _) | TestOutputList (s, _)
+  | TestOutputListOfDevices (s, _)
+  | TestOutputInt (s, _) | TestOutputTrue s | TestOutputFalse s
+  | TestOutputLength (s, _) | TestOutputStruct (s, _)
+  | TestLastFail s -> s
 
-  pr "  if (failed > 0) {\n";
-  pr "    printf (\"***** %%d / %%d tests FAILED *****\\n\", failed, nr_tests);\n";
-  pr "    exit (1);\n";
-  pr "  }\n";
-  pr "\n";
+(* Check function names etc. for consistency. *)
+let check_functions () =
+  let contains_uppercase str =
+    let len = String.length str in
+    let rec loop i =
+      if i >= len then false
+      else (
+       let c = str.[i] in
+       if c >= 'A' && c <= 'Z' then true
+       else loop (i+1)
+      )
+    in
+    loop 0
+  in
 
-  pr "  exit (0);\n";
-  pr "}\n"
+  (* Check function names. *)
+  List.iter (
+    fun (name, _, _, _, _, _, _) ->
+      if String.length name >= 7 && String.sub name 0 7 = "guestfs" then
+       failwithf "function name %s does not need 'guestfs' prefix" name;
+      if name = "" then
+       failwithf "function name is empty";
+      if name.[0] < 'a' || name.[0] > 'z' then
+       failwithf "function name %s must start with lowercase a-z" name;
+      if String.contains name '-' then
+       failwithf "function name %s should not contain '-', use '_' instead."
+         name
+  ) all_functions;
 
-and generate_one_test name i (init, test) =
-  let test_name = sprintf "test_%s_%d" name i in
+  (* Check function parameter/return names. *)
+  List.iter (
+    fun (name, style, _, _, _, _, _) ->
+      let check_arg_ret_name n =
+       if contains_uppercase n then
+         failwithf "%s param/ret %s should not contain uppercase chars"
+           name n;
+       if String.contains n '-' || String.contains n '_' then
+         failwithf "%s param/ret %s should not contain '-' or '_'"
+           name n;
+       if n = "value" then
+         failwithf "%s has a param/ret called 'value', which causes conflicts in the OCaml bindings, use something like 'val' or a more descriptive name" name;
+       if n = "int" || n = "char" || n = "short" || n = "long" then
+         failwithf "%s has a param/ret which conflicts with a C type (eg. 'int', 'char' etc.)" name;
+       if n = "i" || n = "n" then
+         failwithf "%s has a param/ret called 'i' or 'n', which will cause some conflicts in the generated code" name;
+       if n = "argv" || n = "args" then
+         failwithf "%s has a param/ret called 'argv' or 'args', which will cause some conflicts in the generated code" name
+      in
 
-  pr "static int %s (void)\n" test_name;
-  pr "{\n";
+      (match fst style with
+       | RErr -> ()
+       | RInt n | RInt64 n | RBool n | RConstString n | RString n
+       | RStringList n | RPVList n | RVGList n | RLVList n
+       | RStat n | RStatVFS n
+       | RHashtable n ->
+          check_arg_ret_name n
+       | RIntBool (n,m) ->
+          check_arg_ret_name n;
+          check_arg_ret_name m
+      );
+      List.iter (fun arg -> check_arg_ret_name (name_of_argt arg)) (snd style)
+  ) all_functions;
 
-  (match init with
-   | InitNone -> ()
-   | InitEmpty ->
-       pr "  /* InitEmpty for %s (%d) */\n" name i;
-       List.iter (generate_test_command_call test_name)
-        [["umount_all"];
-         ["lvm_remove_all"]]
-   | InitBasicFS ->
-       pr "  /* InitBasicFS for %s (%d): create ext2 on /dev/sda1 */\n" name i;
-       List.iter (generate_test_command_call test_name)
-        [["umount_all"];
-         ["lvm_remove_all"];
-         ["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
-         ["mkfs"; "ext2"; "/dev/sda1"];
-         ["mount"; "/dev/sda1"; "/"]]
-   | InitBasicFSonLVM ->
-       pr "  /* InitBasicFSonLVM for %s (%d): create ext2 on /dev/VG/LV */\n"
-        name i;
-       List.iter (generate_test_command_call test_name)
-        [["umount_all"];
-         ["lvm_remove_all"];
-         ["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
-         ["pvcreate"; "/dev/sda1"];
-         ["vgcreate"; "VG"; "/dev/sda1"];
-         ["lvcreate"; "LV"; "VG"; "8"];
-         ["mkfs"; "ext2"; "/dev/VG/LV"];
-         ["mount"; "/dev/VG/LV"; "/"]]
-  );
-
-  let get_seq_last = function
-    | [] ->
-       failwithf "%s: you cannot use [] (empty list) when expecting a command"
-         test_name
-    | seq ->
-       let seq = List.rev seq in
-       List.rev (List.tl seq), List.hd seq
-  in
-
-  (match test with
-   | TestRun seq ->
-       pr "  /* TestRun for %s (%d) */\n" name i;
-       List.iter (generate_test_command_call test_name) seq
-   | TestOutput (seq, expected) ->
-       pr "  /* TestOutput for %s (%d) */\n" name i;
-       let seq, last = get_seq_last seq in
-       let test () =
-        pr "    if (strcmp (r, \"%s\") != 0) {\n" (c_quote expected);
-        pr "      fprintf (stderr, \"%s: expected \\\"%s\\\" but got \\\"%%s\\\"\\n\", r);\n" test_name (c_quote expected);
-        pr "      return -1;\n";
-        pr "    }\n"
-       in
-       List.iter (generate_test_command_call test_name) seq;
-       generate_test_command_call ~test test_name last
-   | TestOutputList (seq, expected) ->
-       pr "  /* TestOutputList for %s (%d) */\n" name i;
-       let seq, last = get_seq_last seq in
-       let test () =
-        iteri (
-          fun i str ->
-            pr "    if (!r[%d]) {\n" i;
-            pr "      fprintf (stderr, \"%s: short list returned from command\\n\");\n" test_name;
-            pr "      print_strings (r);\n";
-            pr "      return -1;\n";
-            pr "    }\n";
-            pr "    if (strcmp (r[%d], \"%s\") != 0) {\n" i (c_quote str);
-            pr "      fprintf (stderr, \"%s: expected \\\"%s\\\" but got \\\"%%s\\\"\\n\", r[%d]);\n" test_name (c_quote str) i;
-            pr "      return -1;\n";
-            pr "    }\n"
-        ) expected;
-        pr "    if (r[%d] != NULL) {\n" (List.length expected);
-        pr "      fprintf (stderr, \"%s: extra elements returned from command\\n\");\n"
-          test_name;
-        pr "      print_strings (r);\n";
-        pr "      return -1;\n";
-        pr "    }\n"
-       in
-       List.iter (generate_test_command_call test_name) seq;
-       generate_test_command_call ~test test_name last
-   | TestOutputInt (seq, expected) ->
-       pr "  /* TestOutputInt for %s (%d) */\n" name i;
-       let seq, last = get_seq_last seq in
-       let test () =
-        pr "    if (r != %d) {\n" expected;
-        pr "      fprintf (stderr, \"%s: expected %d but got %%d\\n\","
-          test_name expected;
-        pr "               (int) r);\n";
-        pr "      return -1;\n";
-        pr "    }\n"
-       in
-       List.iter (generate_test_command_call test_name) seq;
-       generate_test_command_call ~test test_name last
-   | TestOutputTrue seq ->
-       pr "  /* TestOutputTrue for %s (%d) */\n" name i;
-       let seq, last = get_seq_last seq in
-       let test () =
-        pr "    if (!r) {\n";
-        pr "      fprintf (stderr, \"%s: expected true, got false\\n\");\n"
-          test_name;
-        pr "      return -1;\n";
-        pr "    }\n"
-       in
-       List.iter (generate_test_command_call test_name) seq;
-       generate_test_command_call ~test test_name last
-   | TestOutputFalse seq ->
-       pr "  /* TestOutputFalse for %s (%d) */\n" name i;
-       let seq, last = get_seq_last seq in
-       let test () =
-        pr "    if (r) {\n";
-        pr "      fprintf (stderr, \"%s: expected false, got true\\n\");\n"
-          test_name;
-        pr "      return -1;\n";
-        pr "    }\n"
-       in
-       List.iter (generate_test_command_call test_name) seq;
-       generate_test_command_call ~test test_name last
-   | TestOutputLength (seq, expected) ->
-       pr "  /* TestOutputLength for %s (%d) */\n" name i;
-       let seq, last = get_seq_last seq in
-       let test () =
-        pr "    int j;\n";
-        pr "    for (j = 0; j < %d; ++j)\n" expected;
-        pr "      if (r[j] == NULL) {\n";
-        pr "        fprintf (stderr, \"%s: short list returned\\n\");\n"
-          test_name;
-        pr "        print_strings (r);\n";
-        pr "        return -1;\n";
-        pr "      }\n";
-        pr "    if (r[j] != NULL) {\n";
-        pr "      fprintf (stderr, \"%s: long list returned\\n\");\n"
-          test_name;
-        pr "      print_strings (r);\n";
-        pr "      return -1;\n";
-        pr "    }\n"
-       in
-       List.iter (generate_test_command_call test_name) seq;
-       generate_test_command_call ~test test_name last
-   | TestOutputStruct (seq, checks) ->
-       pr "  /* TestOutputStruct for %s (%d) */\n" name i;
-       let seq, last = get_seq_last seq in
-       let test () =
-        List.iter (
-          function
-          | CompareWithInt (field, expected) ->
-              pr "    if (r->%s != %d) {\n" field expected;
-              pr "      fprintf (stderr, \"%s: %s was %%d, expected %d\\n\",\n"
-                test_name field expected;
-              pr "               (int) r->%s);\n" field;
-              pr "      return -1;\n";
-              pr "    }\n"
-          | CompareWithString (field, expected) ->
-              pr "    if (strcmp (r->%s, \"%s\") != 0) {\n" field expected;
-              pr "      fprintf (stderr, \"%s: %s was \"%%s\", expected \"%s\"\\n\",\n"
-                test_name field expected;
-              pr "               r->%s);\n" field;
-              pr "      return -1;\n";
-              pr "    }\n"
-          | CompareFieldsIntEq (field1, field2) ->
-              pr "    if (r->%s != r->%s) {\n" field1 field2;
-              pr "      fprintf (stderr, \"%s: %s (%%d) <> %s (%%d)\\n\",\n"
-                test_name field1 field2;
-              pr "               (int) r->%s, (int) r->%s);\n" field1 field2;
-              pr "      return -1;\n";
-              pr "    }\n"
-          | CompareFieldsStrEq (field1, field2) ->
-              pr "    if (strcmp (r->%s, r->%s) != 0) {\n" field1 field2;
-              pr "      fprintf (stderr, \"%s: %s (\"%%s\") <> %s (\"%%s\")\\n\",\n"
-                test_name field1 field2;
-              pr "               r->%s, r->%s);\n" field1 field2;
-              pr "      return -1;\n";
-              pr "    }\n"
-        ) checks
-       in
-       List.iter (generate_test_command_call test_name) seq;
-       generate_test_command_call ~test test_name last
-   | TestLastFail seq ->
-       pr "  /* TestLastFail for %s (%d) */\n" name i;
-       let seq, last = get_seq_last seq in
-       List.iter (generate_test_command_call test_name) seq;
-       generate_test_command_call test_name ~expect_error:true last
-  );
-
-  pr "  return 0;\n";
-  pr "}\n";
-  pr "\n";
-  test_name
-
-(* Generate the code to run a command, leaving the result in 'r'.
- * If you expect to get an error then you should set expect_error:true.
- *)
-and generate_test_command_call ?(expect_error = false) ?test test_name cmd =
-  match cmd with
-  | [] -> assert false
-  | name :: args ->
-      (* Look up the command to find out what args/ret it has. *)
-      let style =
-       try
-         let _, style, _, _, _, _, _ =
-           List.find (fun (n, _, _, _, _, _, _) -> n = name) all_functions in
-         style
-       with Not_found ->
-         failwithf "%s: in test, command %s was not found" test_name name in
+  (* Check short descriptions. *)
+  List.iter (
+    fun (name, _, _, _, _, shortdesc, _) ->
+      if shortdesc.[0] <> Char.lowercase shortdesc.[0] then
+       failwithf "short description of %s should begin with lowercase." name;
+      let c = shortdesc.[String.length shortdesc-1] in
+      if c = '\n' || c = '.' then
+       failwithf "short description of %s should not end with . or \\n." name
+  ) all_functions;
 
-      if List.length (snd style) <> List.length args then
-       failwithf "%s: in test, wrong number of args given to %s"
-         test_name name;
+  (* Check long dscriptions. *)
+  List.iter (
+    fun (name, _, _, _, _, _, longdesc) ->
+      if longdesc.[String.length longdesc-1] = '\n' then
+       failwithf "long description of %s should not end with \\n." name
+  ) all_functions;
 
-      pr "  {\n";
+  (* Check proc_nrs. *)
+  List.iter (
+    fun (name, _, proc_nr, _, _, _, _) ->
+      if proc_nr <= 0 then
+       failwithf "daemon function %s should have proc_nr > 0" name
+  ) daemon_functions;
 
-      List.iter (
-       function
-       | String _, _
-       | OptString _, _
-       | Int _, _
-       | Bool _, _ -> ()
-       | FileIn _, _ | FileOut _, _ -> ()
-       | StringList n, arg ->
-           pr "    char *%s[] = {\n" n;
-           let strs = string_split " " arg in
-           List.iter (
-             fun str -> pr "      \"%s\",\n" (c_quote str)
-           ) strs;
-           pr "      NULL\n";
-           pr "    };\n";
-      ) (List.combine (snd style) args);
+  List.iter (
+    fun (name, _, proc_nr, _, _, _, _) ->
+      if proc_nr <> -1 then
+       failwithf "non-daemon function %s should have proc_nr -1" name
+  ) non_daemon_functions;
 
-      let error_code =
-       match fst style with
-       | RErr | RInt _ | RBool _ -> pr "    int r;\n"; "-1"
-       | RInt64 _ -> pr "    int64_t r;\n"; "-1"
-       | RConstString _ -> pr "    const char *r;\n"; "NULL"
-       | RString _ -> pr "    char *r;\n"; "NULL"
-       | RStringList _ | RHashtable _ ->
-           pr "    char **r;\n";
-           pr "    int i;\n";
-           "NULL"
-       | RIntBool _ ->
-           pr "    struct guestfs_int_bool *r;\n"; "NULL"
-       | RPVList _ ->
-           pr "    struct guestfs_lvm_pv_list *r;\n"; "NULL"
-       | RVGList _ ->
-           pr "    struct guestfs_lvm_vg_list *r;\n"; "NULL"
-       | RLVList _ ->
-           pr "    struct guestfs_lvm_lv_list *r;\n"; "NULL"
-       | RStat _ ->
-           pr "    struct guestfs_stat *r;\n"; "NULL"
-       | RStatVFS _ ->
-           pr "    struct guestfs_statvfs *r;\n"; "NULL" in
+  let proc_nrs =
+    List.map (fun (name, _, proc_nr, _, _, _, _) -> name, proc_nr)
+      daemon_functions in
+  let proc_nrs =
+    List.sort (fun (_,nr1) (_,nr2) -> compare nr1 nr2) proc_nrs in
+  let rec loop = function
+    | [] -> ()
+    | [_] -> ()
+    | (name1,nr1) :: ((name2,nr2) :: _ as rest) when nr1 < nr2 ->
+       loop rest
+    | (name1,nr1) :: (name2,nr2) :: _ ->
+       failwithf "%s and %s have conflicting procedure numbers (%d, %d)"
+         name1 name2 nr1 nr2
+  in
+  loop proc_nrs;
 
-      pr "    suppress_error = %d;\n" (if expect_error then 1 else 0);
-      pr "    r = guestfs_%s (g" name;
+  (* Check tests. *)
+  List.iter (
+    function
+      (* Ignore functions that have no tests.  We generate a
+       * warning when the user does 'make check' instead.
+       *)
+    | name, _, _, _, [], _, _ -> ()
+    | name, _, _, _, tests, _, _ ->
+       let funcs =
+         List.map (
+           fun (_, _, test) ->
+             match seq_of_test test with
+             | [] ->
+                 failwithf "%s has a test containing an empty sequence" name
+             | cmds -> List.map List.hd cmds
+         ) tests in
+       let funcs = List.flatten funcs in
 
-      (* Generate the parameters. *)
-      List.iter (
-       function
-       | String _, arg
-       | FileIn _, arg | FileOut _, arg ->
-           pr ", \"%s\"" (c_quote arg)
-       | OptString _, arg ->
-           if arg = "NULL" then pr ", NULL" else pr ", \"%s\"" (c_quote arg)
-       | StringList n, _ ->
-           pr ", %s" n
-       | Int _, arg ->
-           let i =
-             try int_of_string arg
-             with Failure "int_of_string" ->
-               failwithf "%s: expecting an int, but got '%s'" test_name arg in
-           pr ", %d" i
-       | Bool _, arg ->
-           let b = bool_of_string arg in pr ", %d" (if b then 1 else 0)
-      ) (List.combine (snd style) args);
+       let tested = List.mem name funcs in
 
-      pr ");\n";
-      if not expect_error then
-       pr "    if (r == %s)\n" error_code
-      else
-       pr "    if (r != %s)\n" error_code;
-      pr "      return -1;\n";
+       if not tested then
+         failwithf "function %s has tests but does not test itself" name
+  ) all_functions
 
-      (* Insert the test code. *)
-      (match test with
-       | None -> ()
-       | Some f -> f ()
-      );
+(* 'pr' prints to the current output file. *)
+let chan = ref stdout
+let pr fs = ksprintf (output_string !chan) fs
 
-      (match fst style with
-       | RErr | RInt _ | RInt64 _ | RBool _ | RConstString _ -> ()
-       | RString _ -> pr "    free (r);\n"
-       | RStringList _ | RHashtable _ ->
-          pr "    for (i = 0; r[i] != NULL; ++i)\n";
-          pr "      free (r[i]);\n";
-          pr "    free (r);\n"
-       | RIntBool _ ->
-          pr "    guestfs_free_int_bool (r);\n"
-       | RPVList _ ->
-          pr "    guestfs_free_lvm_pv_list (r);\n"
-       | RVGList _ ->
-          pr "    guestfs_free_lvm_vg_list (r);\n"
-       | RLVList _ ->
-          pr "    guestfs_free_lvm_lv_list (r);\n"
-       | RStat _ | RStatVFS _ ->
-          pr "    free (r);\n"
-      );
+(* Generate a header block in a number of standard styles. *)
+type comment_style = CStyle | HashStyle | OCamlStyle | HaskellStyle
+type license = GPLv2 | LGPLv2
 
-      pr "  }\n"
+let generate_header comment license =
+  let c = match comment with
+    | CStyle ->     pr "/* "; " *"
+    | HashStyle ->  pr "# ";  "#"
+    | OCamlStyle -> pr "(* "; " *"
+    | HaskellStyle -> pr "{- "; "  " in
+  pr "libguestfs generated file\n";
+  pr "%s WARNING: THIS FILE IS GENERATED BY 'src/generator.ml'.\n" c;
+  pr "%s ANY CHANGES YOU MAKE TO THIS FILE WILL BE LOST.\n" c;
+  pr "%s\n" c;
+  pr "%s Copyright (C) 2009 Red Hat Inc.\n" c;
+  pr "%s\n" c;
+  (match license with
+   | GPLv2 ->
+       pr "%s This program is free software; you can redistribute it and/or modify\n" c;
+       pr "%s it under the terms of the GNU General Public License as published by\n" c;
+       pr "%s the Free Software Foundation; either version 2 of the License, or\n" c;
+       pr "%s (at your option) any later version.\n" c;
+       pr "%s\n" c;
+       pr "%s This program is distributed in the hope that it will be useful,\n" c;
+       pr "%s but WITHOUT ANY WARRANTY; without even the implied warranty of\n" c;
+       pr "%s MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n" c;
+       pr "%s GNU General Public License for more details.\n" c;
+       pr "%s\n" c;
+       pr "%s You should have received a copy of the GNU General Public License along\n" c;
+       pr "%s with this program; if not, write to the Free Software Foundation, Inc.,\n" c;
+       pr "%s 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.\n" c;
 
-and c_quote str =
-  let str = replace_str str "\r" "\\r" in
-  let str = replace_str str "\n" "\\n" in
-  let str = replace_str str "\t" "\\t" in
-  str
+   | LGPLv2 ->
+       pr "%s This library is free software; you can redistribute it and/or\n" c;
+       pr "%s modify it under the terms of the GNU Lesser General Public\n" c;
+       pr "%s License as published by the Free Software Foundation; either\n" c;
+       pr "%s version 2 of the License, or (at your option) any later version.\n" c;
+       pr "%s\n" c;
+       pr "%s This library is distributed in the hope that it will be useful,\n" c;
+       pr "%s but WITHOUT ANY WARRANTY; without even the implied warranty of\n" c;
+       pr "%s MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n" c;
+       pr "%s Lesser General Public License for more details.\n" c;
+       pr "%s\n" c;
+       pr "%s You should have received a copy of the GNU Lesser General Public\n" c;
+       pr "%s License along with this library; if not, write to the Free Software\n" c;
+       pr "%s Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA\n" c;
+  );
+  (match comment with
+   | CStyle -> pr " */\n"
+   | HashStyle -> ()
+   | OCamlStyle -> pr " *)\n"
+   | HaskellStyle -> pr "-}\n"
+  );
+  pr "\n"
 
-(* Generate a lot of different functions for guestfish. *)
-and generate_fish_cmds () =
-  generate_header CStyle GPLv2;
+(* Start of main code generation functions below this line. *)
 
-  let all_functions =
-    List.filter (
-      fun (_, _, _, flags, _, _, _) -> not (List.mem NotInFish flags)
-    ) all_functions in
-  let all_functions_sorted =
-    List.filter (
-      fun (_, _, _, flags, _, _, _) -> not (List.mem NotInFish flags)
-    ) all_functions_sorted in
-
-  pr "#include <stdio.h>\n";
-  pr "#include <stdlib.h>\n";
-  pr "#include <string.h>\n";
-  pr "#include <inttypes.h>\n";
-  pr "\n";
-  pr "#include <guestfs.h>\n";
-  pr "#include \"fish.h\"\n";
-  pr "\n";
-
-  (* list_commands function, which implements guestfish -h *)
-  pr "void list_commands (void)\n";
-  pr "{\n";
-  pr "  printf (\"    %%-16s     %%s\\n\", \"Command\", \"Description\");\n";
-  pr "  list_builtin_commands ();\n";
-  List.iter (
-    fun (name, _, _, flags, _, shortdesc, _) ->
-      let name = replace_char name '_' '-' in
-      pr "  printf (\"%%-20s %%s\\n\", \"%s\", \"%s\");\n"
-       name shortdesc
-  ) all_functions_sorted;
-  pr "  printf (\"    Use -h <cmd> / help <cmd> to show detailed help for a command.\\n\");\n";
-  pr "}\n";
-  pr "\n";
-
-  (* display_command function, which implements guestfish -h cmd *)
-  pr "void display_command (const char *cmd)\n";
-  pr "{\n";
+(* Generate the pod documentation for the C API. *)
+let rec generate_actions_pod () =
   List.iter (
-    fun (name, style, _, flags, _, shortdesc, longdesc) ->
-      let name2 = replace_char name '_' '-' in
-      let alias =
-       try find_map (function FishAlias n -> Some n | _ -> None) flags
-       with Not_found -> name in
-      let longdesc = replace_str longdesc "C<guestfs_" "C<" in
-      let synopsis =
-       match snd style with
-       | [] -> name2
-       | args ->
-           sprintf "%s <%s>"
-             name2 (String.concat "> <" (List.map name_of_argt args)) in
-
-      let warnings =
+    fun (shortname, style, _, flags, _, _, longdesc) ->
+      if not (List.mem NotInDocs flags) then (
+       let name = "guestfs_" ^ shortname in
+       pr "=head2 %s\n\n" name;
+       pr " ";
+       generate_prototype ~extern:false ~handle:"handle" name style;
+       pr "\n\n";
+       pr "%s\n\n" longdesc;
+       (match fst style with
+        | RErr ->
+            pr "This function returns 0 on success or -1 on error.\n\n"
+        | RInt _ ->
+            pr "On error this function returns -1.\n\n"
+        | RInt64 _ ->
+            pr "On error this function returns -1.\n\n"
+        | RBool _ ->
+            pr "This function returns a C truth value on success or -1 on error.\n\n"
+        | RConstString _ ->
+            pr "This function returns a string, or NULL on error.
+The string is owned by the guest handle and must I<not> be freed.\n\n"
+        | RString _ ->
+            pr "This function returns a string, or NULL on error.
+I<The caller must free the returned string after use>.\n\n"
+        | RStringList _ ->
+            pr "This function returns a NULL-terminated array of strings
+(like L<environ(3)>), or NULL if there was an error.
+I<The caller must free the strings and the array after use>.\n\n"
+        | RIntBool _ ->
+            pr "This function returns a C<struct guestfs_int_bool *>,
+or NULL if there was an error.
+I<The caller must call C<guestfs_free_int_bool> after use>.\n\n"
+        | RPVList _ ->
+            pr "This function returns a C<struct guestfs_lvm_pv_list *>
+(see E<lt>guestfs-structs.hE<gt>),
+or NULL if there was an error.
+I<The caller must call C<guestfs_free_lvm_pv_list> after use>.\n\n"
+        | RVGList _ ->
+            pr "This function returns a C<struct guestfs_lvm_vg_list *>
+(see E<lt>guestfs-structs.hE<gt>),
+or NULL if there was an error.
+I<The caller must call C<guestfs_free_lvm_vg_list> after use>.\n\n"
+        | RLVList _ ->
+            pr "This function returns a C<struct guestfs_lvm_lv_list *>
+(see E<lt>guestfs-structs.hE<gt>),
+or NULL if there was an error.
+I<The caller must call C<guestfs_free_lvm_lv_list> after use>.\n\n"
+        | RStat _ ->
+            pr "This function returns a C<struct guestfs_stat *>
+(see L<stat(2)> and E<lt>guestfs-structs.hE<gt>),
+or NULL if there was an error.
+I<The caller must call C<free> after use>.\n\n"
+        | RStatVFS _ ->
+            pr "This function returns a C<struct guestfs_statvfs *>
+(see L<statvfs(2)> and E<lt>guestfs-structs.hE<gt>),
+or NULL if there was an error.
+I<The caller must call C<free> after use>.\n\n"
+        | RHashtable _ ->
+            pr "This function returns a NULL-terminated array of
+strings, or NULL if there was an error.
+The array of strings will always have length C<2n+1>, where
+C<n> keys and values alternate, followed by the trailing NULL entry.
+I<The caller must free the strings and the array after use>.\n\n"
+       );
        if List.mem ProtocolLimitWarning flags then
-         ("\n\n" ^ protocol_limit_warning)
-       else "" in
+         pr "%s\n\n" protocol_limit_warning;
+       if List.mem DangerWillRobinson flags then
+         pr "%s\n\n" danger_will_robinson
+      )
+  ) all_functions_sorted
 
-      (* For DangerWillRobinson commands, we should probably have
-       * guestfish prompt before allowing you to use them (especially
-       * in interactive mode). XXX
-       *)
-      let warnings =
-       warnings ^
-         if List.mem DangerWillRobinson flags then
-           ("\n\n" ^ danger_will_robinson)
-         else "" in
+and generate_structs_pod () =
+  (* LVM structs documentation. *)
+  List.iter (
+    fun (typ, cols) ->
+      pr "=head2 guestfs_lvm_%s\n" typ;
+      pr "\n";
+      pr " struct guestfs_lvm_%s {\n" typ;
+      List.iter (
+       function
+       | name, `String -> pr "  char *%s;\n" name
+       | name, `UUID ->
+           pr "  /* The next field is NOT nul-terminated, be careful when printing it: */\n";
+           pr "  char %s[32];\n" name
+       | name, `Bytes -> pr "  uint64_t %s;\n" name
+       | name, `Int -> pr "  int64_t %s;\n" name
+       | name, `OptPercent ->
+           pr "  /* The next field is [0..100] or -1 meaning 'not present': */\n";
+           pr "  float %s;\n" name
+      ) cols;
+      pr " \n";
+      pr " struct guestfs_lvm_%s_list {\n" typ;
+      pr "   uint32_t len; /* Number of elements in list. */\n";
+      pr "   struct guestfs_lvm_%s *val; /* Elements. */\n" typ;
+      pr " };\n";
+      pr " \n";
+      pr " void guestfs_free_lvm_%s_list (struct guestfs_free_lvm_%s_list *);\n"
+       typ typ;
+      pr "\n"
+  ) ["pv", pv_cols; "vg", vg_cols; "lv", lv_cols]
 
-      let describe_alias =
-       if name <> alias then
-         sprintf "\n\nYou can use '%s' as an alias for this command." alias
-       else "" in
+(* Generate the protocol (XDR) file, 'guestfs_protocol.x' and
+ * indirectly 'guestfs_protocol.h' and 'guestfs_protocol.c'.
+ *
+ * We have to use an underscore instead of a dash because otherwise
+ * rpcgen generates incorrect code.
+ *
+ * This header is NOT exported to clients, but see also generate_structs_h.
+ *)
+and generate_xdr () =
+  generate_header CStyle LGPLv2;
 
-      pr "  if (";
-      pr "strcasecmp (cmd, \"%s\") == 0" name;
-      if name <> name2 then
-       pr " || strcasecmp (cmd, \"%s\") == 0" name2;
-      if name <> alias then
-       pr " || strcasecmp (cmd, \"%s\") == 0" alias;
-      pr ")\n";
-      pr "    pod2text (\"%s - %s\", %S);\n"
-       name2 shortdesc
-       (" " ^ synopsis ^ "\n\n" ^ longdesc ^ warnings ^ describe_alias);
-      pr "  else\n"
-  ) all_functions;
-  pr "    display_builtin_command (cmd);\n";
-  pr "}\n";
+  (* This has to be defined to get around a limitation in Sun's rpcgen. *)
+  pr "typedef string str<>;\n";
   pr "\n";
 
-  (* print_{pv,vg,lv}_list functions *)
+  (* LVM internal structures. *)
   List.iter (
     function
     | typ, cols ->
-       pr "static void print_%s (struct guestfs_lvm_%s *%s)\n" typ typ typ;
-       pr "{\n";
-       pr "  int i;\n";
-       pr "\n";
-       List.iter (
-         function
-         | name, `String ->
-             pr "  printf (\"%s: %%s\\n\", %s->%s);\n" name typ name
-         | name, `UUID ->
-             pr "  printf (\"%s: \");\n" name;
-             pr "  for (i = 0; i < 32; ++i)\n";
-             pr "    printf (\"%%c\", %s->%s[i]);\n" typ name;
-             pr "  printf (\"\\n\");\n"
-         | name, `Bytes ->
-             pr "  printf (\"%s: %%\" PRIu64 \"\\n\", %s->%s);\n" name typ name
-         | name, `Int ->
-             pr "  printf (\"%s: %%\" PRIi64 \"\\n\", %s->%s);\n" name typ name
-         | name, `OptPercent ->
-             pr "  if (%s->%s >= 0) printf (\"%s: %%g %%%%\\n\", %s->%s);\n"
-               typ name name typ name;
-             pr "  else printf (\"%s: \\n\");\n" name
-       ) cols;
-       pr "}\n";
-       pr "\n";
-       pr "static void print_%s_list (struct guestfs_lvm_%s_list *%ss)\n"
-         typ typ typ;
-       pr "{\n";
-       pr "  int i;\n";
+       pr "struct guestfs_lvm_int_%s {\n" typ;
+       List.iter (function
+                  | name, `String -> pr "  string %s<>;\n" name
+                  | name, `UUID -> pr "  opaque %s[32];\n" name
+                  | name, `Bytes -> pr "  hyper %s;\n" name
+                  | name, `Int -> pr "  hyper %s;\n" name
+                  | name, `OptPercent -> pr "  float %s;\n" name
+                 ) cols;
+       pr "};\n";
        pr "\n";
-       pr "  for (i = 0; i < %ss->len; ++i)\n" typ;
-       pr "    print_%s (&%ss->val[i]);\n" typ typ;
-       pr "}\n";
+       pr "typedef struct guestfs_lvm_int_%s guestfs_lvm_int_%s_list<>;\n" typ typ;
        pr "\n";
   ) ["pv", pv_cols; "vg", vg_cols; "lv", lv_cols];
 
-  (* print_{stat,statvfs} functions *)
+  (* Stat internal structures. *)
   List.iter (
     function
     | typ, cols ->
-       pr "static void print_%s (struct guestfs_%s *%s)\n" typ typ typ;
-       pr "{\n";
-       List.iter (
-         function
-         | name, `Int ->
-             pr "  printf (\"%s: %%\" PRIi64 \"\\n\", %s->%s);\n" name typ name
-       ) cols;
-       pr "}\n";
+       pr "struct guestfs_int_%s {\n" typ;
+       List.iter (function
+                  | name, `Int -> pr "  hyper %s;\n" name
+                 ) cols;
+       pr "};\n";
        pr "\n";
   ) ["stat", stat_cols; "statvfs", statvfs_cols];
 
-  (* run_<action> actions *)
   List.iter (
-    fun (name, style, _, flags, _, _, _) ->
-      pr "static int run_%s (const char *cmd, int argc, char *argv[])\n" name;
-      pr "{\n";
-      (match fst style with
-       | RErr
-       | RInt _
-       | RBool _ -> pr "  int r;\n"
-       | RInt64 _ -> pr "  int64_t r;\n"
-       | RConstString _ -> pr "  const char *r;\n"
-       | RString _ -> pr "  char *r;\n"
-       | RStringList _ | RHashtable _ -> pr "  char **r;\n"
-       | RIntBool _ -> pr "  struct guestfs_int_bool *r;\n"
-       | RPVList _ -> pr "  struct guestfs_lvm_pv_list *r;\n"
-       | RVGList _ -> pr "  struct guestfs_lvm_vg_list *r;\n"
-       | RLVList _ -> pr "  struct guestfs_lvm_lv_list *r;\n"
-       | RStat _ -> pr "  struct guestfs_stat *r;\n"
-       | RStatVFS _ -> pr "  struct guestfs_statvfs *r;\n"
-      );
-      List.iter (
-       function
-       | String n
-       | OptString n
-       | FileIn n
-       | FileOut n -> pr "  const char *%s;\n" n
-       | StringList n -> pr "  char **%s;\n" n
-       | Bool n -> pr "  int %s;\n" n
-       | Int n -> pr "  int %s;\n" n
-      ) (snd style);
+    fun (shortname, style, _, _, _, _, _) ->
+      let name = "guestfs_" ^ shortname in
 
-      (* Check and convert parameters. *)
-      let argc_expected = List.length (snd style) in
-      pr "  if (argc != %d) {\n" argc_expected;
-      pr "    fprintf (stderr, \"%%s should have %d parameter(s)\\n\", cmd);\n"
-       argc_expected;
-      pr "    fprintf (stderr, \"type 'help %%s' for help on %%s\\n\", cmd, cmd);\n";
-      pr "    return -1;\n";
-      pr "  }\n";
-      iteri (
-       fun i ->
-         function
-         | String name -> pr "  %s = argv[%d];\n" name i
-         | OptString name ->
-             pr "  %s = strcmp (argv[%d], \"\") != 0 ? argv[%d] : NULL;\n"
-               name i i
-         | FileIn name ->
-             pr "  %s = strcmp (argv[%d], \"-\") != 0 ? argv[%d] : \"/dev/stdin\";\n"
-               name i i
-         | FileOut name ->
-             pr "  %s = strcmp (argv[%d], \"-\") != 0 ? argv[%d] : \"/dev/stdout\";\n"
-               name i i
-         | StringList name ->
-             pr "  %s = parse_string_list (argv[%d]);\n" name i
-         | Bool name ->
-             pr "  %s = is_true (argv[%d]) ? 1 : 0;\n" name i
-         | Int name ->
-             pr "  %s = atoi (argv[%d]);\n" name i
-      ) (snd style);
-
-      (* Call C API function. *)
-      let fn =
-       try find_map (function FishAction n -> Some n | _ -> None) flags
-       with Not_found -> sprintf "guestfs_%s" name in
-      pr "  r = %s " fn;
-      generate_call_args ~handle:"g" (snd style);
-      pr ";\n";
-
-      (* Check return value for errors and display command results. *)
+      (match snd style with
+       | [] -> ()
+       | args ->
+          pr "struct %s_args {\n" name;
+          List.iter (
+            function
+            | String n -> pr "  string %s<>;\n" n
+            | OptString n -> pr "  str *%s;\n" n
+            | StringList n -> pr "  str %s<>;\n" n
+            | Bool n -> pr "  bool %s;\n" n
+            | Int n -> pr "  int %s;\n" n
+            | FileIn _ | FileOut _ -> ()
+          ) args;
+          pr "};\n\n"
+      );
       (match fst style with
-       | RErr -> pr "  return r;\n"
-       | RInt _ ->
-          pr "  if (r == -1) return -1;\n";
-          pr "  printf (\"%%d\\n\", r);\n";
-          pr "  return 0;\n"
-       | RInt64 _ ->
-          pr "  if (r == -1) return -1;\n";
-          pr "  printf (\"%%\" PRIi64 \"\\n\", r);\n";
-          pr "  return 0;\n"
-       | RBool _ ->
-          pr "  if (r == -1) return -1;\n";
-          pr "  if (r) printf (\"true\\n\"); else printf (\"false\\n\");\n";
-          pr "  return 0;\n"
+       | RErr -> ()
+       | RInt n ->
+          pr "struct %s_ret {\n" name;
+          pr "  int %s;\n" n;
+          pr "};\n\n"
+       | RInt64 n ->
+          pr "struct %s_ret {\n" name;
+          pr "  hyper %s;\n" n;
+          pr "};\n\n"
+       | RBool n ->
+          pr "struct %s_ret {\n" name;
+          pr "  bool %s;\n" n;
+          pr "};\n\n"
        | RConstString _ ->
-          pr "  if (r == NULL) return -1;\n";
-          pr "  printf (\"%%s\\n\", r);\n";
-          pr "  return 0;\n"
-       | RString _ ->
-          pr "  if (r == NULL) return -1;\n";
-          pr "  printf (\"%%s\\n\", r);\n";
-          pr "  free (r);\n";
-          pr "  return 0;\n"
-       | RStringList _ ->
-          pr "  if (r == NULL) return -1;\n";
-          pr "  print_strings (r);\n";
-          pr "  free_strings (r);\n";
-          pr "  return 0;\n"
-       | RIntBool _ ->
-          pr "  if (r == NULL) return -1;\n";
-          pr "  printf (\"%%d, %%s\\n\", r->i,\n";
-          pr "    r->b ? \"true\" : \"false\");\n";
-          pr "  guestfs_free_int_bool (r);\n";
-          pr "  return 0;\n"
-       | RPVList _ ->
-          pr "  if (r == NULL) return -1;\n";
-          pr "  print_pv_list (r);\n";
-          pr "  guestfs_free_lvm_pv_list (r);\n";
-          pr "  return 0;\n"
-       | RVGList _ ->
-          pr "  if (r == NULL) return -1;\n";
-          pr "  print_vg_list (r);\n";
-          pr "  guestfs_free_lvm_vg_list (r);\n";
-          pr "  return 0;\n"
-       | RLVList _ ->
-          pr "  if (r == NULL) return -1;\n";
-          pr "  print_lv_list (r);\n";
-          pr "  guestfs_free_lvm_lv_list (r);\n";
-          pr "  return 0;\n"
-       | RStat _ ->
-          pr "  if (r == NULL) return -1;\n";
-          pr "  print_stat (r);\n";
-          pr "  free (r);\n";
-          pr "  return 0;\n"
-       | RStatVFS _ ->
-          pr "  if (r == NULL) return -1;\n";
-          pr "  print_statvfs (r);\n";
-          pr "  free (r);\n";
-          pr "  return 0;\n"
-       | RHashtable _ ->
-          pr "  if (r == NULL) return -1;\n";
-          pr "  print_table (r);\n";
-          pr "  free_strings (r);\n";
-          pr "  return 0;\n"
+          failwithf "RConstString cannot be returned from a daemon function"
+       | RString n ->
+          pr "struct %s_ret {\n" name;
+          pr "  string %s<>;\n" n;
+          pr "};\n\n"
+       | RStringList n ->
+          pr "struct %s_ret {\n" name;
+          pr "  str %s<>;\n" n;
+          pr "};\n\n"
+       | RIntBool (n,m) ->
+          pr "struct %s_ret {\n" name;
+          pr "  int %s;\n" n;
+          pr "  bool %s;\n" m;
+          pr "};\n\n"
+       | RPVList n ->
+          pr "struct %s_ret {\n" name;
+          pr "  guestfs_lvm_int_pv_list %s;\n" n;
+          pr "};\n\n"
+       | RVGList n ->
+          pr "struct %s_ret {\n" name;
+          pr "  guestfs_lvm_int_vg_list %s;\n" n;
+          pr "};\n\n"
+       | RLVList n ->
+          pr "struct %s_ret {\n" name;
+          pr "  guestfs_lvm_int_lv_list %s;\n" n;
+          pr "};\n\n"
+       | RStat n ->
+          pr "struct %s_ret {\n" name;
+          pr "  guestfs_int_stat %s;\n" n;
+          pr "};\n\n"
+       | RStatVFS n ->
+          pr "struct %s_ret {\n" name;
+          pr "  guestfs_int_statvfs %s;\n" n;
+          pr "};\n\n"
+       | RHashtable n ->
+          pr "struct %s_ret {\n" name;
+          pr "  str %s<>;\n" n;
+          pr "};\n\n"
       );
-      pr "}\n";
-      pr "\n"
-  ) all_functions;
+  ) daemon_functions;
 
-  (* run_action function *)
-  pr "int run_action (const char *cmd, int argc, char *argv[])\n";
-  pr "{\n";
+  (* Table of procedure numbers. *)
+  pr "enum guestfs_procedure {\n";
   List.iter (
-    fun (name, _, _, flags, _, _, _) ->
-      let name2 = replace_char name '_' '-' in
-      let alias =
-       try find_map (function FishAlias n -> Some n | _ -> None) flags
-       with Not_found -> name in
-      pr "  if (";
-      pr "strcasecmp (cmd, \"%s\") == 0" name;
-      if name <> name2 then
-       pr " || strcasecmp (cmd, \"%s\") == 0" name2;
-      if name <> alias then
-       pr " || strcasecmp (cmd, \"%s\") == 0" alias;
-      pr ")\n";
-      pr "    return run_%s (cmd, argc, argv);\n" name;
-      pr "  else\n";
-  ) all_functions;
-  pr "    {\n";
-  pr "      fprintf (stderr, \"%%s: unknown command\\n\", cmd);\n";
-  pr "      return -1;\n";
-  pr "    }\n";
-  pr "  return 0;\n";
-  pr "}\n";
-  pr "\n"
-
-(* Readline completion for guestfish. *)
-and generate_fish_completion () =
-  generate_header CStyle GPLv2;
+    fun (shortname, _, proc_nr, _, _, _, _) ->
+      pr "  GUESTFS_PROC_%s = %d,\n" (String.uppercase shortname) proc_nr
+  ) daemon_functions;
+  pr "  GUESTFS_PROC_NR_PROCS\n";
+  pr "};\n";
+  pr "\n";
 
-  let all_functions =
-    List.filter (
-      fun (_, _, _, flags, _, _, _) -> not (List.mem NotInFish flags)
-    ) all_functions in
+  (* Having to choose a maximum message size is annoying for several
+   * reasons (it limits what we can do in the API), but it (a) makes
+   * the protocol a lot simpler, and (b) provides a bound on the size
+   * of the daemon which operates in limited memory space.  For large
+   * file transfers you should use FTP.
+   *)
+  pr "const GUESTFS_MESSAGE_MAX = %d;\n" (4 * 1024 * 1024);
+  pr "\n";
 
+  (* Message header, etc. *)
   pr "\
-#include <config.h>
+/* The communication protocol is now documented in the guestfs(3)
+ * manpage.
+ */
 
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
+const GUESTFS_PROGRAM = 0x2000F5F5;
+const GUESTFS_PROTOCOL_VERSION = 1;
 
-#ifdef HAVE_LIBREADLINE
-#include <readline/readline.h>
-#endif
+/* These constants must be larger than any possible message length. */
+const GUESTFS_LAUNCH_FLAG = 0xf5f55ff5;
+const GUESTFS_CANCEL_FLAG = 0xffffeeee;
 
-#include \"fish.h\"
+enum guestfs_message_direction {
+  GUESTFS_DIRECTION_CALL = 0,        /* client -> daemon */
+  GUESTFS_DIRECTION_REPLY = 1        /* daemon -> client */
+};
 
-#ifdef HAVE_LIBREADLINE
+enum guestfs_message_status {
+  GUESTFS_STATUS_OK = 0,
+  GUESTFS_STATUS_ERROR = 1
+};
 
-static const char *commands[] = {
-";
+const GUESTFS_ERROR_LEN = 256;
 
-  (* Get the commands and sort them, including the aliases. *)
-  let commands =
-    List.map (
-      fun (name, _, _, flags, _, _, _) ->
-       let name2 = replace_char name '_' '-' in
-       let alias =
-         try find_map (function FishAlias n -> Some n | _ -> None) flags
-         with Not_found -> name in
+struct guestfs_message_error {
+  string error_message<GUESTFS_ERROR_LEN>;
+};
 
-       if name <> alias then [name2; alias] else [name2]
-    ) all_functions in
-  let commands = List.flatten commands in
-  let commands = List.sort compare commands in
+struct guestfs_message_header {
+  unsigned prog;                     /* GUESTFS_PROGRAM */
+  unsigned vers;                     /* GUESTFS_PROTOCOL_VERSION */
+  guestfs_procedure proc;            /* GUESTFS_PROC_x */
+  guestfs_message_direction direction;
+  unsigned serial;                   /* message serial number */
+  guestfs_message_status status;
+};
 
-  List.iter (pr "  \"%s\",\n") commands;
+const GUESTFS_MAX_CHUNK_SIZE = 8192;
 
-  pr "  NULL
+struct guestfs_chunk {
+  int cancel;                       /* if non-zero, transfer is cancelled */
+  /* data size is 0 bytes if the transfer has finished successfully */
+  opaque data<GUESTFS_MAX_CHUNK_SIZE>;
 };
+"
 
-static char *
-generator (const char *text, int state)
-{
-  static int index, len;
-  const char *name;
+(* Generate the guestfs-structs.h file. *)
+and generate_structs_h () =
+  generate_header CStyle LGPLv2;
 
-  if (!state) {
-    index = 0;
-    len = strlen (text);
-  }
+  (* This is a public exported header file containing various
+   * structures.  The structures are carefully written to have
+   * exactly the same in-memory format as the XDR structures that
+   * we use on the wire to the daemon.  The reason for creating
+   * copies of these structures here is just so we don't have to
+   * export the whole of guestfs_protocol.h (which includes much
+   * unrelated and XDR-dependent stuff that we don't want to be
+   * public, or required by clients).
+   *
+   * To reiterate, we will pass these structures to and from the
+   * client with a simple assignment or memcpy, so the format
+   * must be identical to what rpcgen / the RFC defines.
+   *)
 
-  while ((name = commands[index]) != NULL) {
-    index++;
-    if (strncasecmp (name, text, len) == 0)
-      return strdup (name);
-  }
+  (* guestfs_int_bool structure. *)
+  pr "struct guestfs_int_bool {\n";
+  pr "  int32_t i;\n";
+  pr "  int32_t b;\n";
+  pr "};\n";
+  pr "\n";
 
-  return NULL;
-}
+  (* LVM public structures. *)
+  List.iter (
+    function
+    | typ, cols ->
+       pr "struct guestfs_lvm_%s {\n" typ;
+       List.iter (
+         function
+         | name, `String -> pr "  char *%s;\n" name
+         | name, `UUID -> pr "  char %s[32]; /* this is NOT nul-terminated, be careful when printing */\n" name
+         | name, `Bytes -> pr "  uint64_t %s;\n" name
+         | name, `Int -> pr "  int64_t %s;\n" name
+         | name, `OptPercent -> pr "  float %s; /* [0..100] or -1 */\n" name
+       ) cols;
+       pr "};\n";
+       pr "\n";
+       pr "struct guestfs_lvm_%s_list {\n" typ;
+       pr "  uint32_t len;\n";
+       pr "  struct guestfs_lvm_%s *val;\n" typ;
+       pr "};\n";
+       pr "\n"
+  ) ["pv", pv_cols; "vg", vg_cols; "lv", lv_cols];
 
-#endif /* HAVE_LIBREADLINE */
+  (* Stat structures. *)
+  List.iter (
+    function
+    | typ, cols ->
+       pr "struct guestfs_%s {\n" typ;
+       List.iter (
+         function
+         | name, `Int -> pr "  int64_t %s;\n" name
+       ) cols;
+       pr "};\n";
+       pr "\n"
+  ) ["stat", stat_cols; "statvfs", statvfs_cols]
 
-char **do_completion (const char *text, int start, int end)
+(* Generate the guestfs-actions.h file. *)
+and generate_actions_h () =
+  generate_header CStyle LGPLv2;
+  List.iter (
+    fun (shortname, style, _, _, _, _, _) ->
+      let name = "guestfs_" ^ shortname in
+      generate_prototype ~single_line:true ~newline:true ~handle:"handle"
+       name style
+  ) all_functions
+
+(* Generate the client-side dispatch stubs. *)
+and generate_client_actions () =
+  generate_header CStyle LGPLv2;
+
+  pr "\
+#include <stdio.h>
+#include <stdlib.h>
+
+#include \"guestfs.h\"
+#include \"guestfs_protocol.h\"
+
+#define error guestfs_error
+#define perrorf guestfs_perrorf
+#define safe_malloc guestfs_safe_malloc
+#define safe_realloc guestfs_safe_realloc
+#define safe_strdup guestfs_safe_strdup
+#define safe_memdup guestfs_safe_memdup
+
+/* Check the return message from a call for validity. */
+static int
+check_reply_header (guestfs_h *g,
+                    const struct guestfs_message_header *hdr,
+                    int proc_nr, int serial)
 {
-  char **matches = NULL;
+  if (hdr->prog != GUESTFS_PROGRAM) {
+    error (g, \"wrong program (%%d/%%d)\", hdr->prog, GUESTFS_PROGRAM);
+    return -1;
+  }
+  if (hdr->vers != GUESTFS_PROTOCOL_VERSION) {
+    error (g, \"wrong protocol version (%%d/%%d)\",
+          hdr->vers, GUESTFS_PROTOCOL_VERSION);
+    return -1;
+  }
+  if (hdr->direction != GUESTFS_DIRECTION_REPLY) {
+    error (g, \"unexpected message direction (%%d/%%d)\",
+          hdr->direction, GUESTFS_DIRECTION_REPLY);
+    return -1;
+  }
+  if (hdr->proc != proc_nr) {
+    error (g, \"unexpected procedure number (%%d/%%d)\", hdr->proc, proc_nr);
+    return -1;
+  }
+  if (hdr->serial != serial) {
+    error (g, \"unexpected serial (%%d/%%d)\", hdr->serial, serial);
+    return -1;
+  }
 
-#ifdef HAVE_LIBREADLINE
-  if (start == 0)
-    matches = rl_completion_matches (text, generator);
-#endif
+  return 0;
+}
 
-  return matches;
+/* Check we are in the right state to run a high-level action. */
+static int
+check_state (guestfs_h *g, const char *caller)
+{
+  if (!guestfs_is_ready (g)) {
+    if (guestfs_is_config (g))
+      error (g, \"%%s: call launch() before using this function\",
+        caller);
+    else if (guestfs_is_launching (g))
+      error (g, \"%%s: call wait_ready() before using this function\",
+        caller);
+    else
+      error (g, \"%%s called from the wrong state, %%d != READY\",
+        caller, guestfs_get_state (g));
+    return -1;
+  }
+  return 0;
 }
-";
 
-(* Generate the POD documentation for guestfish. *)
-and generate_fish_actions_pod () =
-  let all_functions_sorted =
-    List.filter (
-      fun (_, _, _, flags, _, _, _) -> not (List.mem NotInFish flags)
-    ) all_functions_sorted in
+";
 
+  (* Client-side stubs for each function. *)
   List.iter (
-    fun (name, style, _, flags, _, _, longdesc) ->
-      let longdesc = replace_str longdesc "C<guestfs_" "C<" in
-      let name = replace_char name '_' '-' in
-      let alias =
-       try find_map (function FishAlias n -> Some n | _ -> None) flags
-       with Not_found -> name in
+    fun (shortname, style, _, _, _, _, _) ->
+      let name = "guestfs_" ^ shortname in
 
-      pr "=head2 %s" name;
-      if name <> alias then
-       pr " | %s" alias;
+      (* Generate the context struct which stores the high-level
+       * state between callback functions.
+       *)
+      pr "struct %s_ctx {\n" shortname;
+      pr "  /* This flag is set by the callbacks, so we know we've done\n";
+      pr "   * the callbacks as expected, and in the right sequence.\n";
+      pr "   * 0 = not called, 1 = reply_cb called.\n";
+      pr "   */\n";
+      pr "  int cb_sequence;\n";
+      pr "  struct guestfs_message_header hdr;\n";
+      pr "  struct guestfs_message_error err;\n";
+      (match fst style with
+       | RErr -> ()
+       | RConstString _ ->
+          failwithf "RConstString cannot be returned from a daemon function"
+       | RInt _ | RInt64 _
+       | RBool _ | RString _ | RStringList _
+       | RIntBool _
+       | RPVList _ | RVGList _ | RLVList _
+       | RStat _ | RStatVFS _
+       | RHashtable _ ->
+          pr "  struct %s_ret ret;\n" name
+      );
+      pr "};\n";
       pr "\n";
+
+      (* Generate the reply callback function. *)
+      pr "static void %s_reply_cb (guestfs_h *g, void *data, XDR *xdr)\n" shortname;
+      pr "{\n";
+      pr "  guestfs_main_loop *ml = guestfs_get_main_loop (g);\n";
+      pr "  struct %s_ctx *ctx = (struct %s_ctx *) data;\n" shortname shortname;
       pr "\n";
-      pr " %s" name;
-      List.iter (
-       function
-       | String n -> pr " %s" n
-       | OptString n -> pr " %s" n
-       | StringList n -> pr " %s,..." n
-       | Bool _ -> pr " true|false"
-       | Int n -> pr " %s" n
-       | FileIn n | FileOut n -> pr " (%s|-)" n
-      ) (snd style);
+      pr "  /* This should definitely not happen. */\n";
+      pr "  if (ctx->cb_sequence != 0) {\n";
+      pr "    ctx->cb_sequence = 9999;\n";
+      pr "    error (g, \"%%s: internal error: reply callback called twice\", \"%s\");\n" name;
+      pr "    return;\n";
+      pr "  }\n";
       pr "\n";
+      pr "  ml->main_loop_quit (ml, g);\n";
       pr "\n";
-      pr "%s\n\n" longdesc;
+      pr "  if (!xdr_guestfs_message_header (xdr, &ctx->hdr)) {\n";
+      pr "    error (g, \"%%s: failed to parse reply header\", \"%s\");\n" name;
+      pr "    return;\n";
+      pr "  }\n";
+      pr "  if (ctx->hdr.status == GUESTFS_STATUS_ERROR) {\n";
+      pr "    if (!xdr_guestfs_message_error (xdr, &ctx->err)) {\n";
+      pr "      error (g, \"%%s: failed to parse reply error\", \"%s\");\n"
+       name;
+      pr "      return;\n";
+      pr "    }\n";
+      pr "    goto done;\n";
+      pr "  }\n";
 
-      if List.exists (function FileIn _ | FileOut _ -> true
-                     | _ -> false) (snd style) then
-       pr "Use C<-> instead of a filename to read/write from stdin/stdout.\n\n";
+      (match fst style with
+       | RErr -> ()
+       | RConstString _ ->
+          failwithf "RConstString cannot be returned from a daemon function"
+       | RInt _ | RInt64 _
+       | RBool _ | RString _ | RStringList _
+       | RIntBool _
+       | RPVList _ | RVGList _ | RLVList _
+       | RStat _ | RStatVFS _
+       | RHashtable _ ->
+           pr "  if (!xdr_%s_ret (xdr, &ctx->ret)) {\n" name;
+           pr "    error (g, \"%%s: failed to parse reply\", \"%s\");\n" name;
+           pr "    return;\n";
+           pr "  }\n";
+      );
 
-      if List.mem ProtocolLimitWarning flags then
-       pr "%s\n\n" protocol_limit_warning;
+      pr " done:\n";
+      pr "  ctx->cb_sequence = 1;\n";
+      pr "}\n\n";
 
-      if List.mem DangerWillRobinson flags then
-       pr "%s\n\n" danger_will_robinson
-  ) all_functions_sorted
+      (* Generate the action stub. *)
+      generate_prototype ~extern:false ~semicolon:false ~newline:true
+       ~handle:"g" name style;
 
-(* Generate a C function prototype. *)
-and generate_prototype ?(extern = true) ?(static = false) ?(semicolon = true)
-    ?(single_line = false) ?(newline = false) ?(in_daemon = false)
-    ?(prefix = "")
-    ?handle name style =
-  if extern then pr "extern ";
-  if static then pr "static ";
-  (match fst style with
-   | RErr -> pr "int "
-   | RInt _ -> pr "int "
-   | RInt64 _ -> pr "int64_t "
-   | RBool _ -> pr "int "
-   | RConstString _ -> pr "const char *"
-   | RString _ -> pr "char *"
-   | RStringList _ | RHashtable _ -> pr "char **"
-   | RIntBool _ ->
-       if not in_daemon then pr "struct guestfs_int_bool *"
-       else pr "guestfs_%s_ret *" name
-   | RPVList _ ->
-       if not in_daemon then pr "struct guestfs_lvm_pv_list *"
-       else pr "guestfs_lvm_int_pv_list *"
-   | RVGList _ ->
-       if not in_daemon then pr "struct guestfs_lvm_vg_list *"
-       else pr "guestfs_lvm_int_vg_list *"
-   | RLVList _ ->
-       if not in_daemon then pr "struct guestfs_lvm_lv_list *"
-       else pr "guestfs_lvm_int_lv_list *"
-   | RStat _ ->
-       if not in_daemon then pr "struct guestfs_stat *"
-       else pr "guestfs_int_stat *"
-   | RStatVFS _ ->
-       if not in_daemon then pr "struct guestfs_statvfs *"
-       else pr "guestfs_int_statvfs *"
-  );
-  pr "%s%s (" prefix name;
-  if handle = None && List.length (snd style) = 0 then
-    pr "void"
-  else (
-    let comma = ref false in
-    (match handle with
-     | None -> ()
-     | Some handle -> pr "guestfs_h *%s" handle; comma := true
-    );
-    let next () =
-      if !comma then (
-       if single_line then pr ", " else pr ",\n\t\t"
-      );
-      comma := true
-    in
-    List.iter (
-      function
-      | String n
-      | OptString n -> next (); pr "const char *%s" n
-      | StringList n -> next (); pr "char * const* const %s" n
-      | Bool n -> next (); pr "int %s" n
-      | Int n -> next (); pr "int %s" n
-      | FileIn n
-      | FileOut n ->
-         if not in_daemon then (next (); pr "const char *%s" n)
-    ) (snd style);
-  );
-  pr ")";
-  if semicolon then pr ";";
-  if newline then pr "\n"
-
-(* Generate C call arguments, eg "(handle, foo, bar)" *)
-and generate_call_args ?handle args =
-  pr "(";
-  let comma = ref false in
-  (match handle with
-   | None -> ()
-   | Some handle -> pr "%s" handle; comma := true
-  );
-  List.iter (
-    fun arg ->
-      if !comma then pr ", ";
-      comma := true;
-      pr "%s" (name_of_argt arg)
-  ) args;
-  pr ")"
-
-(* Generate the OCaml bindings interface. *)
-and generate_ocaml_mli () =
-  generate_header OCamlStyle LGPLv2;
+      let error_code =
+       match fst style with
+       | RErr | RInt _ | RInt64 _ | RBool _ -> "-1"
+       | RConstString _ ->
+           failwithf "RConstString cannot be returned from a daemon function"
+       | RString _ | RStringList _ | RIntBool _
+       | RPVList _ | RVGList _ | RLVList _
+       | RStat _ | RStatVFS _
+       | RHashtable _ ->
+           "NULL" in
 
-  pr "\
-(** For API documentation you should refer to the C API
-    in the guestfs(3) manual page.  The OCaml API uses almost
-    exactly the same calls. *)
+      pr "{\n";
 
-type t
-(** A [guestfs_h] handle. *)
+      (match snd style with
+       | [] -> ()
+       | _ -> pr "  struct %s_args args;\n" name
+      );
 
-exception Error of string
-(** This exception is raised when there is an error. *)
+      pr "  struct %s_ctx ctx;\n" shortname;
+      pr "  guestfs_main_loop *ml = guestfs_get_main_loop (g);\n";
+      pr "  int serial;\n";
+      pr "\n";
+      pr "  if (check_state (g, \"%s\") == -1) return %s;\n" name error_code;
+      pr "  guestfs_set_busy (g);\n";
+      pr "\n";
+      pr "  memset (&ctx, 0, sizeof ctx);\n";
+      pr "\n";
 
-val create : unit -> t
+      (* Send the main header and arguments. *)
+      (match snd style with
+       | [] ->
+          pr "  serial = guestfs__send_sync (g, GUESTFS_PROC_%s, NULL, NULL);\n"
+            (String.uppercase shortname)
+       | args ->
+          List.iter (
+            function
+            | String n ->
+                pr "  args.%s = (char *) %s;\n" n n
+            | OptString n ->
+                pr "  args.%s = %s ? (char **) &%s : NULL;\n" n n n
+            | StringList n ->
+                pr "  args.%s.%s_val = (char **) %s;\n" n n n;
+                pr "  for (args.%s.%s_len = 0; %s[args.%s.%s_len]; args.%s.%s_len++) ;\n" n n n n n n n;
+            | Bool n ->
+                pr "  args.%s = %s;\n" n n
+            | Int n ->
+                pr "  args.%s = %s;\n" n n
+            | FileIn _ | FileOut _ -> ()
+          ) args;
+          pr "  serial = guestfs__send_sync (g, GUESTFS_PROC_%s,\n"
+            (String.uppercase shortname);
+          pr "        (xdrproc_t) xdr_%s_args, (char *) &args);\n"
+            name;
+      );
+      pr "  if (serial == -1) {\n";
+      pr "    guestfs_end_busy (g);\n";
+      pr "    return %s;\n" error_code;
+      pr "  }\n";
+      pr "\n";
 
-val close : t -> unit
-(** Handles are closed by the garbage collector when they become
-    unreferenced, but callers can also call this in order to
-    provide predictable cleanup. *)
+      (* Send any additional files (FileIn) requested. *)
+      let need_read_reply_label = ref false in
+      List.iter (
+       function
+       | FileIn n ->
+           pr "  {\n";
+           pr "    int r;\n";
+           pr "\n";
+           pr "    r = guestfs__send_file_sync (g, %s);\n" n;
+           pr "    if (r == -1) {\n";
+           pr "      guestfs_end_busy (g);\n";
+           pr "      return %s;\n" error_code;
+           pr "    }\n";
+           pr "    if (r == -2) /* daemon cancelled */\n";
+           pr "      goto read_reply;\n";
+           need_read_reply_label := true;
+           pr "  }\n";
+           pr "\n";
+       | _ -> ()
+      ) (snd style);
 
-";
-  generate_ocaml_lvm_structure_decls ();
+      (* Wait for the reply from the remote end. *)
+      if !need_read_reply_label then pr " read_reply:\n";
+      pr "  guestfs__switch_to_receiving (g);\n";
+      pr "  ctx.cb_sequence = 0;\n";
+      pr "  guestfs_set_reply_callback (g, %s_reply_cb, &ctx);\n" shortname;
+      pr "  (void) ml->main_loop_run (ml, g);\n";
+      pr "  guestfs_set_reply_callback (g, NULL, NULL);\n";
+      pr "  if (ctx.cb_sequence != 1) {\n";
+      pr "    error (g, \"%%s reply failed, see earlier error messages\", \"%s\");\n" name;
+      pr "    guestfs_end_busy (g);\n";
+      pr "    return %s;\n" error_code;
+      pr "  }\n";
+      pr "\n";
 
-  generate_ocaml_stat_structure_decls ();
+      pr "  if (check_reply_header (g, &ctx.hdr, GUESTFS_PROC_%s, serial) == -1) {\n"
+       (String.uppercase shortname);
+      pr "    guestfs_end_busy (g);\n";
+      pr "    return %s;\n" error_code;
+      pr "  }\n";
+      pr "\n";
 
-  (* The actions. *)
-  List.iter (
-    fun (name, style, _, _, _, shortdesc, _) ->
-      generate_ocaml_prototype name style;
-      pr "(** %s *)\n" shortdesc;
-      pr "\n"
-  ) all_functions
+      pr "  if (ctx.hdr.status == GUESTFS_STATUS_ERROR) {\n";
+      pr "    error (g, \"%%s\", ctx.err.error_message);\n";
+      pr "    free (ctx.err.error_message);\n";
+      pr "    guestfs_end_busy (g);\n";
+      pr "    return %s;\n" error_code;
+      pr "  }\n";
+      pr "\n";
 
-(* Generate the OCaml bindings implementation. *)
-and generate_ocaml_ml () =
-  generate_header OCamlStyle LGPLv2;
+      (* Expecting to receive further files (FileOut)? *)
+      List.iter (
+       function
+       | FileOut n ->
+           pr "  if (guestfs__receive_file_sync (g, %s) == -1) {\n" n;
+           pr "    guestfs_end_busy (g);\n";
+           pr "    return %s;\n" error_code;
+           pr "  }\n";
+           pr "\n";
+       | _ -> ()
+      ) (snd style);
 
-  pr "\
-type t
-exception Error of string
-external create : unit -> t = \"ocaml_guestfs_create\"
-external close : t -> unit = \"ocaml_guestfs_close\"
+      pr "  guestfs_end_busy (g);\n";
 
-let () =
-  Callback.register_exception \"ocaml_guestfs_error\" (Error \"\")
+      (match fst style with
+       | RErr -> pr "  return 0;\n"
+       | RInt n | RInt64 n | RBool n ->
+          pr "  return ctx.ret.%s;\n" n
+       | RConstString _ ->
+          failwithf "RConstString cannot be returned from a daemon function"
+       | RString n ->
+          pr "  return ctx.ret.%s; /* caller will free */\n" n
+       | RStringList n | RHashtable n ->
+          pr "  /* caller will free this, but we need to add a NULL entry */\n";
+          pr "  ctx.ret.%s.%s_val =\n" n n;
+          pr "    safe_realloc (g, ctx.ret.%s.%s_val,\n" n n;
+          pr "                  sizeof (char *) * (ctx.ret.%s.%s_len + 1));\n"
+            n n;
+          pr "  ctx.ret.%s.%s_val[ctx.ret.%s.%s_len] = NULL;\n" n n n n;
+          pr "  return ctx.ret.%s.%s_val;\n" n n
+       | RIntBool _ ->
+          pr "  /* caller with free this */\n";
+          pr "  return safe_memdup (g, &ctx.ret, sizeof (ctx.ret));\n"
+       | RPVList n | RVGList n | RLVList n
+       | RStat n | RStatVFS n ->
+          pr "  /* caller will free this */\n";
+          pr "  return safe_memdup (g, &ctx.ret.%s, sizeof (ctx.ret.%s));\n" n n
+      );
 
-";
+      pr "}\n\n"
+  ) daemon_functions
 
-  generate_ocaml_lvm_structure_decls ();
+(* Generate daemon/actions.h. *)
+and generate_daemon_actions_h () =
+  generate_header CStyle GPLv2;
 
-  generate_ocaml_stat_structure_decls ();
+  pr "#include \"../src/guestfs_protocol.h\"\n";
+  pr "\n";
 
-  (* The actions. *)
   List.iter (
-    fun (name, style, _, _, _, shortdesc, _) ->
-      generate_ocaml_prototype ~is_external:true name style;
-  ) all_functions
+    fun (name, style, _, _, _, _, _) ->
+       generate_prototype
+         ~single_line:true ~newline:true ~in_daemon:true ~prefix:"do_"
+         name style;
+  ) daemon_functions
 
-(* Generate the OCaml bindings C implementation. *)
-and generate_ocaml_c () =
-  generate_header CStyle LGPLv2;
+(* Generate the server-side stubs. *)
+and generate_daemon_actions () =
+  generate_header CStyle GPLv2;
 
-  pr "\
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
+  pr "#include <config.h>\n";
+  pr "\n";
+  pr "#include <stdio.h>\n";
+  pr "#include <stdlib.h>\n";
+  pr "#include <string.h>\n";
+  pr "#include <inttypes.h>\n";
+  pr "#include <ctype.h>\n";
+  pr "#include <rpc/types.h>\n";
+  pr "#include <rpc/xdr.h>\n";
+  pr "\n";
+  pr "#include \"daemon.h\"\n";
+  pr "#include \"../src/guestfs_protocol.h\"\n";
+  pr "#include \"actions.h\"\n";
+  pr "\n";
 
-#include <caml/config.h>
-#include <caml/alloc.h>
-#include <caml/callback.h>
-#include <caml/fail.h>
-#include <caml/memory.h>
-#include <caml/mlvalues.h>
-#include <caml/signals.h>
+  List.iter (
+    fun (name, style, _, _, _, _, _) ->
+      (* Generate server-side stubs. *)
+      pr "static void %s_stub (XDR *xdr_in)\n" name;
+      pr "{\n";
+      let error_code =
+       match fst style with
+       | RErr | RInt _ -> pr "  int r;\n"; "-1"
+       | RInt64 _ -> pr "  int64_t r;\n"; "-1"
+       | RBool _ -> pr "  int r;\n"; "-1"
+       | RConstString _ ->
+           failwithf "RConstString cannot be returned from a daemon function"
+       | RString _ -> pr "  char *r;\n"; "NULL"
+       | RStringList _ | RHashtable _ -> pr "  char **r;\n"; "NULL"
+       | RIntBool _ -> pr "  guestfs_%s_ret *r;\n" name; "NULL"
+       | RPVList _ -> pr "  guestfs_lvm_int_pv_list *r;\n"; "NULL"
+       | RVGList _ -> pr "  guestfs_lvm_int_vg_list *r;\n"; "NULL"
+       | RLVList _ -> pr "  guestfs_lvm_int_lv_list *r;\n"; "NULL"
+       | RStat _ -> pr "  guestfs_int_stat *r;\n"; "NULL"
+       | RStatVFS _ -> pr "  guestfs_int_statvfs *r;\n"; "NULL" in
 
-#include <guestfs.h>
-
-#include \"guestfs_c.h\"
-
-/* Copy a hashtable of string pairs into an assoc-list.  We return
- * the list in reverse order, but hashtables aren't supposed to be
- * ordered anyway.
- */
-static CAMLprim value
-copy_table (char * const * argv)
-{
-  CAMLparam0 ();
-  CAMLlocal5 (rv, pairv, kv, vv, cons);
-  int i;
+      (match snd style with
+       | [] -> ()
+       | args ->
+          pr "  struct guestfs_%s_args args;\n" name;
+          List.iter (
+            function
+              (* Note we allow the string to be writable, in order to
+               * allow device name translation.  This is safe because
+               * we can modify the string (passed from RPC).
+               *)
+            | String n
+            | OptString n -> pr "  char *%s;\n" n
+            | StringList n -> pr "  char **%s;\n" n
+            | Bool n -> pr "  int %s;\n" n
+            | Int n -> pr "  int %s;\n" n
+            | FileIn _ | FileOut _ -> ()
+          ) args
+      );
+      pr "\n";
 
-  rv = Val_int (0);
-  for (i = 0; argv[i] != NULL; i += 2) {
-    kv = caml_copy_string (argv[i]);
-    vv = caml_copy_string (argv[i+1]);
-    pairv = caml_alloc (2, 0);
-    Store_field (pairv, 0, kv);
-    Store_field (pairv, 1, vv);
-    cons = caml_alloc (2, 0);
-    Store_field (cons, 1, rv);
-    rv = cons;
-    Store_field (cons, 0, pairv);
-  }
+      (match snd style with
+       | [] -> ()
+       | args ->
+          pr "  memset (&args, 0, sizeof args);\n";
+          pr "\n";
+          pr "  if (!xdr_guestfs_%s_args (xdr_in, &args)) {\n" name;
+          pr "    reply_with_error (\"%%s: daemon failed to decode procedure arguments\", \"%s\");\n" name;
+          pr "    return;\n";
+          pr "  }\n";
+          List.iter (
+            function
+            | String n -> pr "  %s = args.%s;\n" n n
+            | OptString n -> pr "  %s = args.%s ? *args.%s : NULL;\n" n n n
+            | StringList n ->
+                pr "  %s = realloc (args.%s.%s_val,\n" n n n;
+                pr "                sizeof (char *) * (args.%s.%s_len+1));\n" n n;
+                pr "  if (%s == NULL) {\n" n;
+                pr "    reply_with_perror (\"realloc\");\n";
+                pr "    goto done;\n";
+                pr "  }\n";
+                pr "  %s[args.%s.%s_len] = NULL;\n" n n n;
+                pr "  args.%s.%s_val = %s;\n" n n n;
+            | Bool n -> pr "  %s = args.%s;\n" n n
+            | Int n -> pr "  %s = args.%s;\n" n n
+            | FileIn _ | FileOut _ -> ()
+          ) args;
+          pr "\n"
+      );
 
-  CAMLreturn (rv);
-}
+      (* Don't want to call the impl with any FileIn or FileOut
+       * parameters, since these go "outside" the RPC protocol.
+       *)
+      let argsnofile =
+       List.filter (function FileIn _ | FileOut _ -> false | _ -> true)
+         (snd style) in
+      pr "  r = do_%s " name;
+      generate_call_args argsnofile;
+      pr ";\n";
 
-";
+      pr "  if (r == %s)\n" error_code;
+      pr "    /* do_%s has already called reply_with_error */\n" name;
+      pr "    goto done;\n";
+      pr "\n";
 
-  (* LVM struct copy functions. *)
-  List.iter (
-    fun (typ, cols) ->
-      let has_optpercent_col =
-       List.exists (function (_, `OptPercent) -> true | _ -> false) cols in
+      (* If there are any FileOut parameters, then the impl must
+       * send its own reply.
+       *)
+      let no_reply =
+       List.exists (function FileOut _ -> true | _ -> false) (snd style) in
+      if no_reply then
+       pr "  /* do_%s has already sent a reply */\n" name
+      else (
+       match fst style with
+       | RErr -> pr "  reply (NULL, NULL);\n"
+       | RInt n | RInt64 n | RBool n ->
+           pr "  struct guestfs_%s_ret ret;\n" name;
+           pr "  ret.%s = r;\n" n;
+           pr "  reply ((xdrproc_t) &xdr_guestfs_%s_ret, (char *) &ret);\n"
+             name
+       | RConstString _ ->
+           failwithf "RConstString cannot be returned from a daemon function"
+       | RString n ->
+           pr "  struct guestfs_%s_ret ret;\n" name;
+           pr "  ret.%s = r;\n" n;
+           pr "  reply ((xdrproc_t) &xdr_guestfs_%s_ret, (char *) &ret);\n"
+             name;
+           pr "  free (r);\n"
+       | RStringList n | RHashtable n ->
+           pr "  struct guestfs_%s_ret ret;\n" name;
+           pr "  ret.%s.%s_len = count_strings (r);\n" n n;
+           pr "  ret.%s.%s_val = r;\n" n n;
+           pr "  reply ((xdrproc_t) &xdr_guestfs_%s_ret, (char *) &ret);\n"
+             name;
+           pr "  free_strings (r);\n"
+       | RIntBool _ ->
+           pr "  reply ((xdrproc_t) xdr_guestfs_%s_ret, (char *) r);\n"
+             name;
+           pr "  xdr_free ((xdrproc_t) xdr_guestfs_%s_ret, (char *) r);\n" name
+       | RPVList n | RVGList n | RLVList n
+       | RStat n | RStatVFS n ->
+           pr "  struct guestfs_%s_ret ret;\n" name;
+           pr "  ret.%s = *r;\n" n;
+           pr "  reply ((xdrproc_t) xdr_guestfs_%s_ret, (char *) &ret);\n"
+             name;
+           pr "  xdr_free ((xdrproc_t) xdr_guestfs_%s_ret, (char *) &ret);\n"
+             name
+      );
 
-      pr "static CAMLprim value\n";
-      pr "copy_lvm_%s (const struct guestfs_lvm_%s *%s)\n" typ typ typ;
-      pr "{\n";
-      pr "  CAMLparam0 ();\n";
-      if has_optpercent_col then
-       pr "  CAMLlocal3 (rv, v, v2);\n"
-      else
-       pr "  CAMLlocal2 (rv, v);\n";
-      pr "\n";
-      pr "  rv = caml_alloc (%d, 0);\n" (List.length cols);
-      iteri (
-       fun i col ->
-         (match col with
-          | name, `String ->
-              pr "  v = caml_copy_string (%s->%s);\n" typ name
-          | name, `UUID ->
-              pr "  v = caml_alloc_string (32);\n";
-              pr "  memcpy (String_val (v), %s->%s, 32);\n" typ name
-          | name, `Bytes
-          | name, `Int ->
-              pr "  v = caml_copy_int64 (%s->%s);\n" typ name
-          | name, `OptPercent ->
-              pr "  if (%s->%s >= 0) { /* Some %s */\n" typ name name;
-              pr "    v2 = caml_copy_double (%s->%s);\n" typ name;
-              pr "    v = caml_alloc (1, 0);\n";
-              pr "    Store_field (v, 0, v2);\n";
-              pr "  } else /* None */\n";
-              pr "    v = Val_int (0);\n";
-         );
-         pr "  Store_field (rv, %d, v);\n" i
-      ) cols;
-      pr "  CAMLreturn (rv);\n";
-      pr "}\n";
-      pr "\n";
+      (* Free the args. *)
+      (match snd style with
+       | [] ->
+          pr "done: ;\n";
+       | _ ->
+          pr "done:\n";
+          pr "  xdr_free ((xdrproc_t) xdr_guestfs_%s_args, (char *) &args);\n"
+            name
+      );
 
-      pr "static CAMLprim value\n";
-      pr "copy_lvm_%s_list (const struct guestfs_lvm_%s_list *%ss)\n"
-       typ typ typ;
-      pr "{\n";
-      pr "  CAMLparam0 ();\n";
-      pr "  CAMLlocal2 (rv, v);\n";
-      pr "  int i;\n";
-      pr "\n";
-      pr "  if (%ss->len == 0)\n" typ;
-      pr "    CAMLreturn (Atom (0));\n";
-      pr "  else {\n";
-      pr "    rv = caml_alloc (%ss->len, 0);\n" typ;
-      pr "    for (i = 0; i < %ss->len; ++i) {\n" typ;
-      pr "      v = copy_lvm_%s (&%ss->val[i]);\n" typ typ;
-      pr "      caml_modify (&Field (rv, i), v);\n";
-      pr "    }\n";
-      pr "    CAMLreturn (rv);\n";
-      pr "  }\n";
-      pr "}\n";
-      pr "\n";
-  ) ["pv", pv_cols; "vg", vg_cols; "lv", lv_cols];
+      pr "}\n\n";
+  ) daemon_functions;
 
-  (* Stat copy functions. *)
-  List.iter (
-    fun (typ, cols) ->
-      pr "static CAMLprim value\n";
-      pr "copy_%s (const struct guestfs_%s *%s)\n" typ typ typ;
-      pr "{\n";
-      pr "  CAMLparam0 ();\n";
-      pr "  CAMLlocal2 (rv, v);\n";
-      pr "\n";
-      pr "  rv = caml_alloc (%d, 0);\n" (List.length cols);
-      iteri (
-       fun i col ->
-         (match col with
-          | name, `Int ->
-              pr "  v = caml_copy_int64 (%s->%s);\n" typ name
-         );
-         pr "  Store_field (rv, %d, v);\n" i
-      ) cols;
-      pr "  CAMLreturn (rv);\n";
-      pr "}\n";
-      pr "\n";
-  ) ["stat", stat_cols; "statvfs", statvfs_cols];
+  (* Dispatch function. *)
+  pr "void dispatch_incoming_message (XDR *xdr_in)\n";
+  pr "{\n";
+  pr "  switch (proc_nr) {\n";
 
-  (* The wrappers. *)
   List.iter (
     fun (name, style, _, _, _, _, _) ->
-      let params =
-       "gv" :: List.map (fun arg -> name_of_argt arg ^ "v") (snd style) in
+       pr "    case GUESTFS_PROC_%s:\n" (String.uppercase name);
+       pr "      %s_stub (xdr_in);\n" name;
+       pr "      break;\n"
+  ) daemon_functions;
 
-      pr "CAMLprim value\n";
-      pr "ocaml_guestfs_%s (value %s" name (List.hd params);
-      List.iter (pr ", value %s") (List.tl params);
-      pr ")\n";
-      pr "{\n";
+  pr "    default:\n";
+  pr "      reply_with_error (\"dispatch_incoming_message: unknown procedure number %%d, set LIBGUESTFS_PATH to point to the matching libguestfs appliance directory\", proc_nr);\n";
+  pr "  }\n";
+  pr "}\n";
+  pr "\n";
 
-      (match params with
-       | p1 :: p2 :: p3 :: p4 :: p5 :: rest ->
-          pr "  CAMLparam5 (%s);\n" (String.concat ", " [p1; p2; p3; p4; p5]);
-          pr "  CAMLxparam%d (%s);\n"
-            (List.length rest) (String.concat ", " rest)
-       | ps ->
-          pr "  CAMLparam%d (%s);\n" (List.length ps) (String.concat ", " ps)
-      );
-      pr "  CAMLlocal1 (rv);\n";
-      pr "\n";
+  (* LVM columns and tokenization functions. *)
+  (* XXX This generates crap code.  We should rethink how we
+   * do this parsing.
+   *)
+  List.iter (
+    function
+    | typ, cols ->
+       pr "static const char *lvm_%s_cols = \"%s\";\n"
+         typ (String.concat "," (List.map fst cols));
+       pr "\n";
 
-      pr "  guestfs_h *g = Guestfs_val (gv);\n";
-      pr "  if (g == NULL)\n";
-      pr "    caml_failwith (\"%s: used handle after closing it\");\n" name;
-      pr "\n";
+       pr "static int lvm_tokenize_%s (char *str, struct guestfs_lvm_int_%s *r)\n" typ typ;
+       pr "{\n";
+       pr "  char *tok, *p, *next;\n";
+       pr "  int i, j;\n";
+       pr "\n";
+       (*
+       pr "  fprintf (stderr, \"%%s: <<%%s>>\\n\", __func__, str);\n";
+       pr "\n";
+       *)
+       pr "  if (!str) {\n";
+       pr "    fprintf (stderr, \"%%s: failed: passed a NULL string\\n\", __func__);\n";
+       pr "    return -1;\n";
+       pr "  }\n";
+       pr "  if (!*str || isspace (*str)) {\n";
+       pr "    fprintf (stderr, \"%%s: failed: passed a empty string or one beginning with whitespace\\n\", __func__);\n";
+       pr "    return -1;\n";
+       pr "  }\n";
+       pr "  tok = str;\n";
+       List.iter (
+         fun (name, coltype) ->
+           pr "  if (!tok) {\n";
+           pr "    fprintf (stderr, \"%%s: failed: string finished early, around token %%s\\n\", __func__, \"%s\");\n" name;
+           pr "    return -1;\n";
+           pr "  }\n";
+           pr "  p = strchrnul (tok, ',');\n";
+           pr "  if (*p) next = p+1; else next = NULL;\n";
+           pr "  *p = '\\0';\n";
+           (match coltype with
+            | `String ->
+                pr "  r->%s = strdup (tok);\n" name;
+                pr "  if (r->%s == NULL) {\n" name;
+                pr "    perror (\"strdup\");\n";
+                pr "    return -1;\n";
+                pr "  }\n"
+            | `UUID ->
+                pr "  for (i = j = 0; i < 32; ++j) {\n";
+                pr "    if (tok[j] == '\\0') {\n";
+                pr "      fprintf (stderr, \"%%s: failed to parse UUID from '%%s'\\n\", __func__, tok);\n";
+                pr "      return -1;\n";
+                pr "    } else if (tok[j] != '-')\n";
+                pr "      r->%s[i++] = tok[j];\n" name;
+                pr "  }\n";
+            | `Bytes ->
+                pr "  if (sscanf (tok, \"%%\"SCNu64, &r->%s) != 1) {\n" name;
+                pr "    fprintf (stderr, \"%%s: failed to parse size '%%s' from token %%s\\n\", __func__, tok, \"%s\");\n" name;
+                pr "    return -1;\n";
+                pr "  }\n";
+            | `Int ->
+                pr "  if (sscanf (tok, \"%%\"SCNi64, &r->%s) != 1) {\n" name;
+                pr "    fprintf (stderr, \"%%s: failed to parse int '%%s' from token %%s\\n\", __func__, tok, \"%s\");\n" name;
+                pr "    return -1;\n";
+                pr "  }\n";
+            | `OptPercent ->
+                pr "  if (tok[0] == '\\0')\n";
+                pr "    r->%s = -1;\n" name;
+                pr "  else if (sscanf (tok, \"%%f\", &r->%s) != 1) {\n" name;
+                pr "    fprintf (stderr, \"%%s: failed to parse float '%%s' from token %%s\\n\", __func__, tok, \"%s\");\n" name;
+                pr "    return -1;\n";
+                pr "  }\n";
+           );
+           pr "  tok = next;\n";
+       ) cols;
+
+       pr "  if (tok != NULL) {\n";
+       pr "    fprintf (stderr, \"%%s: failed: extra tokens at end of string\\n\", __func__);\n";
+       pr "    return -1;\n";
+       pr "  }\n";
+       pr "  return 0;\n";
+       pr "}\n";
+       pr "\n";
+
+       pr "guestfs_lvm_int_%s_list *\n" typ;
+       pr "parse_command_line_%ss (void)\n" typ;
+       pr "{\n";
+       pr "  char *out, *err;\n";
+       pr "  char *p, *pend;\n";
+       pr "  int r, i;\n";
+       pr "  guestfs_lvm_int_%s_list *ret;\n" typ;
+       pr "  void *newp;\n";
+       pr "\n";
+       pr "  ret = malloc (sizeof *ret);\n";
+       pr "  if (!ret) {\n";
+       pr "    reply_with_perror (\"malloc\");\n";
+       pr "    return NULL;\n";
+       pr "  }\n";
+       pr "\n";
+       pr "  ret->guestfs_lvm_int_%s_list_len = 0;\n" typ;
+       pr "  ret->guestfs_lvm_int_%s_list_val = NULL;\n" typ;
+       pr "\n";
+       pr "  r = command (&out, &err,\n";
+       pr "           \"/sbin/lvm\", \"%ss\",\n" typ;
+       pr "           \"-o\", lvm_%s_cols, \"--unbuffered\", \"--noheadings\",\n" typ;
+       pr "           \"--nosuffix\", \"--separator\", \",\", \"--units\", \"b\", NULL);\n";
+       pr "  if (r == -1) {\n";
+       pr "    reply_with_error (\"%%s\", err);\n";
+       pr "    free (out);\n";
+       pr "    free (err);\n";
+       pr "    free (ret);\n";
+       pr "    return NULL;\n";
+       pr "  }\n";
+       pr "\n";
+       pr "  free (err);\n";
+       pr "\n";
+       pr "  /* Tokenize each line of the output. */\n";
+       pr "  p = out;\n";
+       pr "  i = 0;\n";
+       pr "  while (p) {\n";
+       pr "    pend = strchr (p, '\\n');       /* Get the next line of output. */\n";
+       pr "    if (pend) {\n";
+       pr "      *pend = '\\0';\n";
+       pr "      pend++;\n";
+       pr "    }\n";
+       pr "\n";
+       pr "    while (*p && isspace (*p))      /* Skip any leading whitespace. */\n";
+       pr "      p++;\n";
+       pr "\n";
+       pr "    if (!*p) {                      /* Empty line?  Skip it. */\n";
+       pr "      p = pend;\n";
+       pr "      continue;\n";
+       pr "    }\n";
+       pr "\n";
+       pr "    /* Allocate some space to store this next entry. */\n";
+       pr "    newp = realloc (ret->guestfs_lvm_int_%s_list_val,\n" typ;
+       pr "                sizeof (guestfs_lvm_int_%s) * (i+1));\n" typ;
+       pr "    if (newp == NULL) {\n";
+       pr "      reply_with_perror (\"realloc\");\n";
+       pr "      free (ret->guestfs_lvm_int_%s_list_val);\n" typ;
+       pr "      free (ret);\n";
+       pr "      free (out);\n";
+       pr "      return NULL;\n";
+       pr "    }\n";
+       pr "    ret->guestfs_lvm_int_%s_list_val = newp;\n" typ;
+       pr "\n";
+       pr "    /* Tokenize the next entry. */\n";
+       pr "    r = lvm_tokenize_%s (p, &ret->guestfs_lvm_int_%s_list_val[i]);\n" typ typ;
+       pr "    if (r == -1) {\n";
+       pr "      reply_with_error (\"failed to parse output of '%ss' command\");\n" typ;
+        pr "      free (ret->guestfs_lvm_int_%s_list_val);\n" typ;
+        pr "      free (ret);\n";
+       pr "      free (out);\n";
+       pr "      return NULL;\n";
+       pr "    }\n";
+       pr "\n";
+       pr "    ++i;\n";
+       pr "    p = pend;\n";
+       pr "  }\n";
+       pr "\n";
+       pr "  ret->guestfs_lvm_int_%s_list_len = i;\n" typ;
+       pr "\n";
+       pr "  free (out);\n";
+       pr "  return ret;\n";
+       pr "}\n"
+
+  ) ["pv", pv_cols; "vg", vg_cols; "lv", lv_cols]
+
+(* Generate the tests. *)
+and generate_tests () =
+  generate_header CStyle GPLv2;
+
+  pr "\
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <fcntl.h>
+
+#include \"guestfs.h\"
+
+static guestfs_h *g;
+static int suppress_error = 0;
+
+static void print_error (guestfs_h *g, void *data, const char *msg)
+{
+  if (!suppress_error)
+    fprintf (stderr, \"%%s\\n\", msg);
+}
+
+static void print_strings (char * const * const argv)
+{
+  int argc;
+
+  for (argc = 0; argv[argc] != NULL; ++argc)
+    printf (\"\\t%%s\\n\", argv[argc]);
+}
+
+/*
+static void print_table (char * const * const argv)
+{
+  int i;
+
+  for (i = 0; argv[i] != NULL; i += 2)
+    printf (\"%%s: %%s\\n\", argv[i], argv[i+1]);
+}
+*/
+
+static void no_test_warnings (void)
+{
+";
+
+  List.iter (
+    function
+    | name, _, _, _, [], _, _ ->
+       pr "  fprintf (stderr, \"warning: \\\"guestfs_%s\\\" has no tests\\n\");\n" name
+    | name, _, _, _, tests, _, _ -> ()
+  ) all_functions;
+
+  pr "}\n";
+  pr "\n";
+
+  (* Generate the actual tests.  Note that we generate the tests
+   * in reverse order, deliberately, so that (in general) the
+   * newest tests run first.  This makes it quicker and easier to
+   * debug them.
+   *)
+  let test_names =
+    List.map (
+      fun (name, _, _, _, tests, _, _) ->
+       mapi (generate_one_test name) tests
+    ) (List.rev all_functions) in
+  let test_names = List.concat test_names in
+  let nr_tests = List.length test_names in
+
+  pr "\
+int main (int argc, char *argv[])
+{
+  char c = 0;
+  int failed = 0;
+  const char *filename;
+  int fd;
+  int nr_tests, test_num = 0;
+
+  no_test_warnings ();
+
+  g = guestfs_create ();
+  if (g == NULL) {
+    printf (\"guestfs_create FAILED\\n\");
+    exit (1);
+  }
+
+  guestfs_set_error_handler (g, print_error, NULL);
+
+  guestfs_set_path (g, \"../appliance\");
+
+  filename = \"test1.img\";
+  fd = open (filename, O_WRONLY|O_CREAT|O_NOCTTY|O_NONBLOCK|O_TRUNC, 0666);
+  if (fd == -1) {
+    perror (filename);
+    exit (1);
+  }
+  if (lseek (fd, %d, SEEK_SET) == -1) {
+    perror (\"lseek\");
+    close (fd);
+    unlink (filename);
+    exit (1);
+  }
+  if (write (fd, &c, 1) == -1) {
+    perror (\"write\");
+    close (fd);
+    unlink (filename);
+    exit (1);
+  }
+  if (close (fd) == -1) {
+    perror (filename);
+    unlink (filename);
+    exit (1);
+  }
+  if (guestfs_add_drive (g, filename) == -1) {
+    printf (\"guestfs_add_drive %%s FAILED\\n\", filename);
+    exit (1);
+  }
+
+  filename = \"test2.img\";
+  fd = open (filename, O_WRONLY|O_CREAT|O_NOCTTY|O_NONBLOCK|O_TRUNC, 0666);
+  if (fd == -1) {
+    perror (filename);
+    exit (1);
+  }
+  if (lseek (fd, %d, SEEK_SET) == -1) {
+    perror (\"lseek\");
+    close (fd);
+    unlink (filename);
+    exit (1);
+  }
+  if (write (fd, &c, 1) == -1) {
+    perror (\"write\");
+    close (fd);
+    unlink (filename);
+    exit (1);
+  }
+  if (close (fd) == -1) {
+    perror (filename);
+    unlink (filename);
+    exit (1);
+  }
+  if (guestfs_add_drive (g, filename) == -1) {
+    printf (\"guestfs_add_drive %%s FAILED\\n\", filename);
+    exit (1);
+  }
+
+  filename = \"test3.img\";
+  fd = open (filename, O_WRONLY|O_CREAT|O_NOCTTY|O_NONBLOCK|O_TRUNC, 0666);
+  if (fd == -1) {
+    perror (filename);
+    exit (1);
+  }
+  if (lseek (fd, %d, SEEK_SET) == -1) {
+    perror (\"lseek\");
+    close (fd);
+    unlink (filename);
+    exit (1);
+  }
+  if (write (fd, &c, 1) == -1) {
+    perror (\"write\");
+    close (fd);
+    unlink (filename);
+    exit (1);
+  }
+  if (close (fd) == -1) {
+    perror (filename);
+    unlink (filename);
+    exit (1);
+  }
+  if (guestfs_add_drive (g, filename) == -1) {
+    printf (\"guestfs_add_drive %%s FAILED\\n\", filename);
+    exit (1);
+  }
+
+  if (guestfs_add_drive_ro (g, \"../images/test.sqsh\") == -1) {
+    printf (\"guestfs_add_drive_ro ../images/test.sqsh FAILED\\n\");
+    exit (1);
+  }
+
+  if (guestfs_launch (g) == -1) {
+    printf (\"guestfs_launch FAILED\\n\");
+    exit (1);
+  }
+
+  /* Set a timeout in case qemu hangs during launch (RHBZ#505329). */
+  alarm (600);
+
+  if (guestfs_wait_ready (g) == -1) {
+    printf (\"guestfs_wait_ready FAILED\\n\");
+    exit (1);
+  }
+
+  /* Cancel previous alarm. */
+  alarm (0);
+
+  nr_tests = %d;
+
+" (500 * 1024 * 1024) (50 * 1024 * 1024) (10 * 1024 * 1024) nr_tests;
+
+  iteri (
+    fun i test_name ->
+      pr "  test_num++;\n";
+      pr "  printf (\"%%3d/%%3d %s\\n\", test_num, nr_tests);\n" test_name;
+      pr "  if (%s () == -1) {\n" test_name;
+      pr "    printf (\"%s FAILED\\n\");\n" test_name;
+      pr "    failed++;\n";
+      pr "  }\n";
+  ) test_names;
+  pr "\n";
+
+  pr "  guestfs_close (g);\n";
+  pr "  unlink (\"test1.img\");\n";
+  pr "  unlink (\"test2.img\");\n";
+  pr "  unlink (\"test3.img\");\n";
+  pr "\n";
+
+  pr "  if (failed > 0) {\n";
+  pr "    printf (\"***** %%d / %%d tests FAILED *****\\n\", failed, nr_tests);\n";
+  pr "    exit (1);\n";
+  pr "  }\n";
+  pr "\n";
+
+  pr "  exit (0);\n";
+  pr "}\n"
+
+and generate_one_test name i (init, prereq, test) =
+  let test_name = sprintf "test_%s_%d" name i in
+
+  pr "\
+static int %s_skip (void)
+{
+  const char *str;
+
+  str = getenv (\"TEST_ONLY\");
+  if (str)
+    return strstr (str, \"%s\") == NULL;
+  str = getenv (\"SKIP_%s\");
+  if (str && strcmp (str, \"1\") == 0) return 1;
+  str = getenv (\"SKIP_TEST_%s\");
+  if (str && strcmp (str, \"1\") == 0) return 1;
+  return 0;
+}
+
+" test_name name (String.uppercase test_name) (String.uppercase name);
+
+  (match prereq with
+   | Disabled | Always -> ()
+   | If code | Unless code ->
+       pr "static int %s_prereq (void)\n" test_name;
+       pr "{\n";
+       pr "  %s\n" code;
+       pr "}\n";
+       pr "\n";
+  );
+
+  pr "\
+static int %s (void)
+{
+  if (%s_skip ()) {
+    printf (\"%%s skipped (reason: environment variable set)\\n\", \"%s\");
+    return 0;
+  }
+
+" test_name test_name test_name;
+
+  (match prereq with
+   | Disabled ->
+       pr "  printf (\"%%s skipped (reason: test disabled in generator)\\n\", \"%s\");\n" test_name
+   | If _ ->
+       pr "  if (! %s_prereq ()) {\n" test_name;
+       pr "    printf (\"%%s skipped (reason: test prerequisite)\\n\", \"%s\");\n" test_name;
+       pr "    return 0;\n";
+       pr "  }\n";
+       pr "\n";
+       generate_one_test_body name i test_name init test;
+   | Unless _ ->
+       pr "  if (%s_prereq ()) {\n" test_name;
+       pr "    printf (\"%%s skipped (reason: test prerequisite)\\n\", \"%s\");\n" test_name;
+       pr "    return 0;\n";
+       pr "  }\n";
+       pr "\n";
+       generate_one_test_body name i test_name init test;
+   | Always ->
+       generate_one_test_body name i test_name init test
+  );
+
+  pr "  return 0;\n";
+  pr "}\n";
+  pr "\n";
+  test_name
+
+and generate_one_test_body name i test_name init test =
+  (match init with
+   | InitNone
+   | InitEmpty ->
+       pr "  /* InitNone|InitEmpty for %s */\n" test_name;
+       List.iter (generate_test_command_call test_name)
+        [["blockdev_setrw"; "/dev/sda"];
+         ["umount_all"];
+         ["lvm_remove_all"]]
+   | InitBasicFS ->
+       pr "  /* InitBasicFS for %s: create ext2 on /dev/sda1 */\n" test_name;
+       List.iter (generate_test_command_call test_name)
+        [["blockdev_setrw"; "/dev/sda"];
+         ["umount_all"];
+         ["lvm_remove_all"];
+         ["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
+         ["mkfs"; "ext2"; "/dev/sda1"];
+         ["mount"; "/dev/sda1"; "/"]]
+   | InitBasicFSonLVM ->
+       pr "  /* InitBasicFSonLVM for %s: create ext2 on /dev/VG/LV */\n"
+        test_name;
+       List.iter (generate_test_command_call test_name)
+        [["blockdev_setrw"; "/dev/sda"];
+         ["umount_all"];
+         ["lvm_remove_all"];
+         ["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
+         ["pvcreate"; "/dev/sda1"];
+         ["vgcreate"; "VG"; "/dev/sda1"];
+         ["lvcreate"; "LV"; "VG"; "8"];
+         ["mkfs"; "ext2"; "/dev/VG/LV"];
+         ["mount"; "/dev/VG/LV"; "/"]]
+  );
+
+  let get_seq_last = function
+    | [] ->
+       failwithf "%s: you cannot use [] (empty list) when expecting a command"
+         test_name
+    | seq ->
+       let seq = List.rev seq in
+       List.rev (List.tl seq), List.hd seq
+  in
+
+  match test with
+  | TestRun seq ->
+      pr "  /* TestRun for %s (%d) */\n" name i;
+      List.iter (generate_test_command_call test_name) seq
+  | TestOutput (seq, expected) ->
+      pr "  /* TestOutput for %s (%d) */\n" name i;
+      pr "  char expected[] = \"%s\";\n" (c_quote expected);
+      let seq, last = get_seq_last seq in
+      let test () =
+       pr "    if (strcmp (r, expected) != 0) {\n";
+       pr "      fprintf (stderr, \"%s: expected \\\"%%s\\\" but got \\\"%%s\\\"\\n\", expected, r);\n" test_name;
+       pr "      return -1;\n";
+       pr "    }\n"
+      in
+      List.iter (generate_test_command_call test_name) seq;
+      generate_test_command_call ~test test_name last
+  | TestOutputList (seq, expected) ->
+      pr "  /* TestOutputList for %s (%d) */\n" name i;
+      let seq, last = get_seq_last seq in
+      let test () =
+       iteri (
+         fun i str ->
+           pr "    if (!r[%d]) {\n" i;
+           pr "      fprintf (stderr, \"%s: short list returned from command\\n\");\n" test_name;
+           pr "      print_strings (r);\n";
+           pr "      return -1;\n";
+           pr "    }\n";
+            pr "    {\n";
+            pr "      char expected[] = \"%s\";\n" (c_quote str);
+           pr "      if (strcmp (r[%d], expected) != 0) {\n" i;
+           pr "        fprintf (stderr, \"%s: expected \\\"%%s\\\" but got \\\"%%s\\\"\\n\", expected, r[%d]);\n" test_name i;
+           pr "        return -1;\n";
+           pr "      }\n";
+           pr "    }\n"
+       ) expected;
+       pr "    if (r[%d] != NULL) {\n" (List.length expected);
+       pr "      fprintf (stderr, \"%s: extra elements returned from command\\n\");\n"
+         test_name;
+       pr "      print_strings (r);\n";
+       pr "      return -1;\n";
+       pr "    }\n"
+      in
+      List.iter (generate_test_command_call test_name) seq;
+      generate_test_command_call ~test test_name last
+  | TestOutputListOfDevices (seq, expected) ->
+      pr "  /* TestOutputListOfDevices for %s (%d) */\n" name i;
+      let seq, last = get_seq_last seq in
+      let test () =
+       iteri (
+         fun i str ->
+           pr "    if (!r[%d]) {\n" i;
+           pr "      fprintf (stderr, \"%s: short list returned from command\\n\");\n" test_name;
+           pr "      print_strings (r);\n";
+           pr "      return -1;\n";
+           pr "    }\n";
+            pr "    {\n";
+            pr "      char expected[] = \"%s\";\n" (c_quote str);
+           pr "      r[%d][5] = 's';\n" i;
+           pr "      if (strcmp (r[%d], expected) != 0) {\n" i;
+           pr "        fprintf (stderr, \"%s: expected \\\"%%s\\\" but got \\\"%%s\\\"\\n\", expected, r[%d]);\n" test_name i;
+           pr "        return -1;\n";
+           pr "      }\n";
+           pr "    }\n"
+       ) expected;
+       pr "    if (r[%d] != NULL) {\n" (List.length expected);
+       pr "      fprintf (stderr, \"%s: extra elements returned from command\\n\");\n"
+         test_name;
+       pr "      print_strings (r);\n";
+       pr "      return -1;\n";
+       pr "    }\n"
+      in
+      List.iter (generate_test_command_call test_name) seq;
+      generate_test_command_call ~test test_name last
+  | TestOutputInt (seq, expected) ->
+      pr "  /* TestOutputInt for %s (%d) */\n" name i;
+      let seq, last = get_seq_last seq in
+      let test () =
+       pr "    if (r != %d) {\n" expected;
+       pr "      fprintf (stderr, \"%s: expected %d but got %%d\\n\","
+         test_name expected;
+       pr "               (int) r);\n";
+       pr "      return -1;\n";
+       pr "    }\n"
+      in
+      List.iter (generate_test_command_call test_name) seq;
+      generate_test_command_call ~test test_name last
+  | TestOutputTrue seq ->
+      pr "  /* TestOutputTrue for %s (%d) */\n" name i;
+      let seq, last = get_seq_last seq in
+      let test () =
+       pr "    if (!r) {\n";
+       pr "      fprintf (stderr, \"%s: expected true, got false\\n\");\n"
+         test_name;
+       pr "      return -1;\n";
+       pr "    }\n"
+      in
+      List.iter (generate_test_command_call test_name) seq;
+      generate_test_command_call ~test test_name last
+  | TestOutputFalse seq ->
+      pr "  /* TestOutputFalse for %s (%d) */\n" name i;
+      let seq, last = get_seq_last seq in
+      let test () =
+       pr "    if (r) {\n";
+       pr "      fprintf (stderr, \"%s: expected false, got true\\n\");\n"
+         test_name;
+       pr "      return -1;\n";
+       pr "    }\n"
+      in
+      List.iter (generate_test_command_call test_name) seq;
+      generate_test_command_call ~test test_name last
+  | TestOutputLength (seq, expected) ->
+      pr "  /* TestOutputLength for %s (%d) */\n" name i;
+      let seq, last = get_seq_last seq in
+      let test () =
+       pr "    int j;\n";
+       pr "    for (j = 0; j < %d; ++j)\n" expected;
+       pr "      if (r[j] == NULL) {\n";
+       pr "        fprintf (stderr, \"%s: short list returned\\n\");\n"
+         test_name;
+       pr "        print_strings (r);\n";
+       pr "        return -1;\n";
+       pr "      }\n";
+       pr "    if (r[j] != NULL) {\n";
+       pr "      fprintf (stderr, \"%s: long list returned\\n\");\n"
+         test_name;
+       pr "      print_strings (r);\n";
+       pr "      return -1;\n";
+       pr "    }\n"
+      in
+      List.iter (generate_test_command_call test_name) seq;
+      generate_test_command_call ~test test_name last
+  | TestOutputStruct (seq, checks) ->
+      pr "  /* TestOutputStruct for %s (%d) */\n" name i;
+      let seq, last = get_seq_last seq in
+      let test () =
+       List.iter (
+         function
+         | CompareWithInt (field, expected) ->
+             pr "    if (r->%s != %d) {\n" field expected;
+             pr "      fprintf (stderr, \"%s: %s was %%d, expected %d\\n\",\n"
+               test_name field expected;
+             pr "               (int) r->%s);\n" field;
+             pr "      return -1;\n";
+             pr "    }\n"
+         | CompareWithString (field, expected) ->
+             pr "    if (strcmp (r->%s, \"%s\") != 0) {\n" field expected;
+             pr "      fprintf (stderr, \"%s: %s was \"%%s\", expected \"%s\"\\n\",\n"
+               test_name field expected;
+             pr "               r->%s);\n" field;
+             pr "      return -1;\n";
+             pr "    }\n"
+         | CompareFieldsIntEq (field1, field2) ->
+             pr "    if (r->%s != r->%s) {\n" field1 field2;
+             pr "      fprintf (stderr, \"%s: %s (%%d) <> %s (%%d)\\n\",\n"
+               test_name field1 field2;
+             pr "               (int) r->%s, (int) r->%s);\n" field1 field2;
+             pr "      return -1;\n";
+             pr "    }\n"
+         | CompareFieldsStrEq (field1, field2) ->
+             pr "    if (strcmp (r->%s, r->%s) != 0) {\n" field1 field2;
+             pr "      fprintf (stderr, \"%s: %s (\"%%s\") <> %s (\"%%s\")\\n\",\n"
+               test_name field1 field2;
+             pr "               r->%s, r->%s);\n" field1 field2;
+             pr "      return -1;\n";
+             pr "    }\n"
+       ) checks
+      in
+      List.iter (generate_test_command_call test_name) seq;
+      generate_test_command_call ~test test_name last
+  | TestLastFail seq ->
+      pr "  /* TestLastFail for %s (%d) */\n" name i;
+      let seq, last = get_seq_last seq in
+      List.iter (generate_test_command_call test_name) seq;
+      generate_test_command_call test_name ~expect_error:true last
+
+(* Generate the code to run a command, leaving the result in 'r'.
+ * If you expect to get an error then you should set expect_error:true.
+ *)
+and generate_test_command_call ?(expect_error = false) ?test test_name cmd =
+  match cmd with
+  | [] -> assert false
+  | name :: args ->
+      (* Look up the command to find out what args/ret it has. *)
+      let style =
+       try
+         let _, style, _, _, _, _, _ =
+           List.find (fun (n, _, _, _, _, _, _) -> n = name) all_functions in
+         style
+       with Not_found ->
+         failwithf "%s: in test, command %s was not found" test_name name in
+
+      if List.length (snd style) <> List.length args then
+       failwithf "%s: in test, wrong number of args given to %s"
+         test_name name;
+
+      pr "  {\n";
+
+      List.iter (
+       function
+       | OptString n, "NULL" -> ()
+       | String n, arg
+       | OptString n, arg ->
+           pr "    char %s[] = \"%s\";\n" n (c_quote arg);
+       | Int _, _
+       | Bool _, _
+       | FileIn _, _ | FileOut _, _ -> ()
+       | StringList n, arg ->
+           let strs = string_split " " arg in
+           iteri (
+             fun i str ->
+                pr "    char %s_%d[] = \"%s\";\n" n i (c_quote str);
+           ) strs;
+           pr "    char *%s[] = {\n" n;
+           iteri (
+             fun i _ -> pr "      %s_%d,\n" n i
+           ) strs;
+           pr "      NULL\n";
+           pr "    };\n";
+      ) (List.combine (snd style) args);
+
+      let error_code =
+       match fst style with
+       | RErr | RInt _ | RBool _ -> pr "    int r;\n"; "-1"
+       | RInt64 _ -> pr "    int64_t r;\n"; "-1"
+       | RConstString _ -> pr "    const char *r;\n"; "NULL"
+       | RString _ -> pr "    char *r;\n"; "NULL"
+       | RStringList _ | RHashtable _ ->
+           pr "    char **r;\n";
+           pr "    int i;\n";
+           "NULL"
+       | RIntBool _ ->
+           pr "    struct guestfs_int_bool *r;\n"; "NULL"
+       | RPVList _ ->
+           pr "    struct guestfs_lvm_pv_list *r;\n"; "NULL"
+       | RVGList _ ->
+           pr "    struct guestfs_lvm_vg_list *r;\n"; "NULL"
+       | RLVList _ ->
+           pr "    struct guestfs_lvm_lv_list *r;\n"; "NULL"
+       | RStat _ ->
+           pr "    struct guestfs_stat *r;\n"; "NULL"
+       | RStatVFS _ ->
+           pr "    struct guestfs_statvfs *r;\n"; "NULL" in
+
+      pr "    suppress_error = %d;\n" (if expect_error then 1 else 0);
+      pr "    r = guestfs_%s (g" name;
+
+      (* Generate the parameters. *)
+      List.iter (
+       function
+       | OptString _, "NULL" -> pr ", NULL"
+       | String n, _
+       | OptString n, _ ->
+            pr ", %s" n
+       | FileIn _, arg | FileOut _, arg ->
+           pr ", \"%s\"" (c_quote arg)
+       | StringList n, _ ->
+           pr ", %s" n
+       | Int _, arg ->
+           let i =
+             try int_of_string arg
+             with Failure "int_of_string" ->
+               failwithf "%s: expecting an int, but got '%s'" test_name arg in
+           pr ", %d" i
+       | Bool _, arg ->
+           let b = bool_of_string arg in pr ", %d" (if b then 1 else 0)
+      ) (List.combine (snd style) args);
+
+      pr ");\n";
+      if not expect_error then
+       pr "    if (r == %s)\n" error_code
+      else
+       pr "    if (r != %s)\n" error_code;
+      pr "      return -1;\n";
+
+      (* Insert the test code. *)
+      (match test with
+       | None -> ()
+       | Some f -> f ()
+      );
+
+      (match fst style with
+       | RErr | RInt _ | RInt64 _ | RBool _ | RConstString _ -> ()
+       | RString _ -> pr "    free (r);\n"
+       | RStringList _ | RHashtable _ ->
+          pr "    for (i = 0; r[i] != NULL; ++i)\n";
+          pr "      free (r[i]);\n";
+          pr "    free (r);\n"
+       | RIntBool _ ->
+          pr "    guestfs_free_int_bool (r);\n"
+       | RPVList _ ->
+          pr "    guestfs_free_lvm_pv_list (r);\n"
+       | RVGList _ ->
+          pr "    guestfs_free_lvm_vg_list (r);\n"
+       | RLVList _ ->
+          pr "    guestfs_free_lvm_lv_list (r);\n"
+       | RStat _ | RStatVFS _ ->
+          pr "    free (r);\n"
+      );
+
+      pr "  }\n"
+
+and c_quote str =
+  let str = replace_str str "\r" "\\r" in
+  let str = replace_str str "\n" "\\n" in
+  let str = replace_str str "\t" "\\t" in
+  let str = replace_str str "\000" "\\0" in
+  str
+
+(* Generate a lot of different functions for guestfish. *)
+and generate_fish_cmds () =
+  generate_header CStyle GPLv2;
+
+  let all_functions =
+    List.filter (
+      fun (_, _, _, flags, _, _, _) -> not (List.mem NotInFish flags)
+    ) all_functions in
+  let all_functions_sorted =
+    List.filter (
+      fun (_, _, _, flags, _, _, _) -> not (List.mem NotInFish flags)
+    ) all_functions_sorted in
+
+  pr "#include <stdio.h>\n";
+  pr "#include <stdlib.h>\n";
+  pr "#include <string.h>\n";
+  pr "#include <inttypes.h>\n";
+  pr "\n";
+  pr "#include <guestfs.h>\n";
+  pr "#include \"fish.h\"\n";
+  pr "\n";
+
+  (* list_commands function, which implements guestfish -h *)
+  pr "void list_commands (void)\n";
+  pr "{\n";
+  pr "  printf (\"    %%-16s     %%s\\n\", \"Command\", \"Description\");\n";
+  pr "  list_builtin_commands ();\n";
+  List.iter (
+    fun (name, _, _, flags, _, shortdesc, _) ->
+      let name = replace_char name '_' '-' in
+      pr "  printf (\"%%-20s %%s\\n\", \"%s\", \"%s\");\n"
+       name shortdesc
+  ) all_functions_sorted;
+  pr "  printf (\"    Use -h <cmd> / help <cmd> to show detailed help for a command.\\n\");\n";
+  pr "}\n";
+  pr "\n";
+
+  (* display_command function, which implements guestfish -h cmd *)
+  pr "void display_command (const char *cmd)\n";
+  pr "{\n";
+  List.iter (
+    fun (name, style, _, flags, _, shortdesc, longdesc) ->
+      let name2 = replace_char name '_' '-' in
+      let alias =
+       try find_map (function FishAlias n -> Some n | _ -> None) flags
+       with Not_found -> name in
+      let longdesc = replace_str longdesc "C<guestfs_" "C<" in
+      let synopsis =
+       match snd style with
+       | [] -> name2
+       | args ->
+           sprintf "%s <%s>"
+             name2 (String.concat "> <" (List.map name_of_argt args)) in
+
+      let warnings =
+       if List.mem ProtocolLimitWarning flags then
+         ("\n\n" ^ protocol_limit_warning)
+       else "" in
+
+      (* For DangerWillRobinson commands, we should probably have
+       * guestfish prompt before allowing you to use them (especially
+       * in interactive mode). XXX
+       *)
+      let warnings =
+       warnings ^
+         if List.mem DangerWillRobinson flags then
+           ("\n\n" ^ danger_will_robinson)
+         else "" in
+
+      let describe_alias =
+       if name <> alias then
+         sprintf "\n\nYou can use '%s' as an alias for this command." alias
+       else "" in
+
+      pr "  if (";
+      pr "strcasecmp (cmd, \"%s\") == 0" name;
+      if name <> name2 then
+       pr " || strcasecmp (cmd, \"%s\") == 0" name2;
+      if name <> alias then
+       pr " || strcasecmp (cmd, \"%s\") == 0" alias;
+      pr ")\n";
+      pr "    pod2text (\"%s - %s\", %S);\n"
+       name2 shortdesc
+       (" " ^ synopsis ^ "\n\n" ^ longdesc ^ warnings ^ describe_alias);
+      pr "  else\n"
+  ) all_functions;
+  pr "    display_builtin_command (cmd);\n";
+  pr "}\n";
+  pr "\n";
+
+  (* print_{pv,vg,lv}_list functions *)
+  List.iter (
+    function
+    | typ, cols ->
+       pr "static void print_%s (struct guestfs_lvm_%s *%s)\n" typ typ typ;
+       pr "{\n";
+       pr "  int i;\n";
+       pr "\n";
+       List.iter (
+         function
+         | name, `String ->
+             pr "  printf (\"%s: %%s\\n\", %s->%s);\n" name typ name
+         | name, `UUID ->
+             pr "  printf (\"%s: \");\n" name;
+             pr "  for (i = 0; i < 32; ++i)\n";
+             pr "    printf (\"%%c\", %s->%s[i]);\n" typ name;
+             pr "  printf (\"\\n\");\n"
+         | name, `Bytes ->
+             pr "  printf (\"%s: %%\" PRIu64 \"\\n\", %s->%s);\n" name typ name
+         | name, `Int ->
+             pr "  printf (\"%s: %%\" PRIi64 \"\\n\", %s->%s);\n" name typ name
+         | name, `OptPercent ->
+             pr "  if (%s->%s >= 0) printf (\"%s: %%g %%%%\\n\", %s->%s);\n"
+               typ name name typ name;
+             pr "  else printf (\"%s: \\n\");\n" name
+       ) cols;
+       pr "}\n";
+       pr "\n";
+       pr "static void print_%s_list (struct guestfs_lvm_%s_list *%ss)\n"
+         typ typ typ;
+       pr "{\n";
+       pr "  int i;\n";
+       pr "\n";
+       pr "  for (i = 0; i < %ss->len; ++i)\n" typ;
+       pr "    print_%s (&%ss->val[i]);\n" typ typ;
+       pr "}\n";
+       pr "\n";
+  ) ["pv", pv_cols; "vg", vg_cols; "lv", lv_cols];
+
+  (* print_{stat,statvfs} functions *)
+  List.iter (
+    function
+    | typ, cols ->
+       pr "static void print_%s (struct guestfs_%s *%s)\n" typ typ typ;
+       pr "{\n";
+       List.iter (
+         function
+         | name, `Int ->
+             pr "  printf (\"%s: %%\" PRIi64 \"\\n\", %s->%s);\n" name typ name
+       ) cols;
+       pr "}\n";
+       pr "\n";
+  ) ["stat", stat_cols; "statvfs", statvfs_cols];
+
+  (* run_<action> actions *)
+  List.iter (
+    fun (name, style, _, flags, _, _, _) ->
+      pr "static int run_%s (const char *cmd, int argc, char *argv[])\n" name;
+      pr "{\n";
+      (match fst style with
+       | RErr
+       | RInt _
+       | RBool _ -> pr "  int r;\n"
+       | RInt64 _ -> pr "  int64_t r;\n"
+       | RConstString _ -> pr "  const char *r;\n"
+       | RString _ -> pr "  char *r;\n"
+       | RStringList _ | RHashtable _ -> pr "  char **r;\n"
+       | RIntBool _ -> pr "  struct guestfs_int_bool *r;\n"
+       | RPVList _ -> pr "  struct guestfs_lvm_pv_list *r;\n"
+       | RVGList _ -> pr "  struct guestfs_lvm_vg_list *r;\n"
+       | RLVList _ -> pr "  struct guestfs_lvm_lv_list *r;\n"
+       | RStat _ -> pr "  struct guestfs_stat *r;\n"
+       | RStatVFS _ -> pr "  struct guestfs_statvfs *r;\n"
+      );
+      List.iter (
+       function
+       | String n
+       | OptString n
+       | FileIn n
+       | FileOut n -> pr "  const char *%s;\n" n
+       | StringList n -> pr "  char **%s;\n" n
+       | Bool n -> pr "  int %s;\n" n
+       | Int n -> pr "  int %s;\n" n
+      ) (snd style);
+
+      (* Check and convert parameters. *)
+      let argc_expected = List.length (snd style) in
+      pr "  if (argc != %d) {\n" argc_expected;
+      pr "    fprintf (stderr, \"%%s should have %d parameter(s)\\n\", cmd);\n"
+       argc_expected;
+      pr "    fprintf (stderr, \"type 'help %%s' for help on %%s\\n\", cmd, cmd);\n";
+      pr "    return -1;\n";
+      pr "  }\n";
+      iteri (
+       fun i ->
+         function
+         | String name -> pr "  %s = argv[%d];\n" name i
+         | OptString name ->
+             pr "  %s = strcmp (argv[%d], \"\") != 0 ? argv[%d] : NULL;\n"
+               name i i
+         | FileIn name ->
+             pr "  %s = strcmp (argv[%d], \"-\") != 0 ? argv[%d] : \"/dev/stdin\";\n"
+               name i i
+         | FileOut name ->
+             pr "  %s = strcmp (argv[%d], \"-\") != 0 ? argv[%d] : \"/dev/stdout\";\n"
+               name i i
+         | StringList name ->
+             pr "  %s = parse_string_list (argv[%d]);\n" name i
+         | Bool name ->
+             pr "  %s = is_true (argv[%d]) ? 1 : 0;\n" name i
+         | Int name ->
+             pr "  %s = atoi (argv[%d]);\n" name i
+      ) (snd style);
+
+      (* Call C API function. *)
+      let fn =
+       try find_map (function FishAction n -> Some n | _ -> None) flags
+       with Not_found -> sprintf "guestfs_%s" name in
+      pr "  r = %s " fn;
+      generate_call_args ~handle:"g" (snd style);
+      pr ";\n";
+
+      (* Check return value for errors and display command results. *)
+      (match fst style with
+       | RErr -> pr "  return r;\n"
+       | RInt _ ->
+          pr "  if (r == -1) return -1;\n";
+          pr "  printf (\"%%d\\n\", r);\n";
+          pr "  return 0;\n"
+       | RInt64 _ ->
+          pr "  if (r == -1) return -1;\n";
+          pr "  printf (\"%%\" PRIi64 \"\\n\", r);\n";
+          pr "  return 0;\n"
+       | RBool _ ->
+          pr "  if (r == -1) return -1;\n";
+          pr "  if (r) printf (\"true\\n\"); else printf (\"false\\n\");\n";
+          pr "  return 0;\n"
+       | RConstString _ ->
+          pr "  if (r == NULL) return -1;\n";
+          pr "  printf (\"%%s\\n\", r);\n";
+          pr "  return 0;\n"
+       | RString _ ->
+          pr "  if (r == NULL) return -1;\n";
+          pr "  printf (\"%%s\\n\", r);\n";
+          pr "  free (r);\n";
+          pr "  return 0;\n"
+       | RStringList _ ->
+          pr "  if (r == NULL) return -1;\n";
+          pr "  print_strings (r);\n";
+          pr "  free_strings (r);\n";
+          pr "  return 0;\n"
+       | RIntBool _ ->
+          pr "  if (r == NULL) return -1;\n";
+          pr "  printf (\"%%d, %%s\\n\", r->i,\n";
+          pr "    r->b ? \"true\" : \"false\");\n";
+          pr "  guestfs_free_int_bool (r);\n";
+          pr "  return 0;\n"
+       | RPVList _ ->
+          pr "  if (r == NULL) return -1;\n";
+          pr "  print_pv_list (r);\n";
+          pr "  guestfs_free_lvm_pv_list (r);\n";
+          pr "  return 0;\n"
+       | RVGList _ ->
+          pr "  if (r == NULL) return -1;\n";
+          pr "  print_vg_list (r);\n";
+          pr "  guestfs_free_lvm_vg_list (r);\n";
+          pr "  return 0;\n"
+       | RLVList _ ->
+          pr "  if (r == NULL) return -1;\n";
+          pr "  print_lv_list (r);\n";
+          pr "  guestfs_free_lvm_lv_list (r);\n";
+          pr "  return 0;\n"
+       | RStat _ ->
+          pr "  if (r == NULL) return -1;\n";
+          pr "  print_stat (r);\n";
+          pr "  free (r);\n";
+          pr "  return 0;\n"
+       | RStatVFS _ ->
+          pr "  if (r == NULL) return -1;\n";
+          pr "  print_statvfs (r);\n";
+          pr "  free (r);\n";
+          pr "  return 0;\n"
+       | RHashtable _ ->
+          pr "  if (r == NULL) return -1;\n";
+          pr "  print_table (r);\n";
+          pr "  free_strings (r);\n";
+          pr "  return 0;\n"
+      );
+      pr "}\n";
+      pr "\n"
+  ) all_functions;
+
+  (* run_action function *)
+  pr "int run_action (const char *cmd, int argc, char *argv[])\n";
+  pr "{\n";
+  List.iter (
+    fun (name, _, _, flags, _, _, _) ->
+      let name2 = replace_char name '_' '-' in
+      let alias =
+       try find_map (function FishAlias n -> Some n | _ -> None) flags
+       with Not_found -> name in
+      pr "  if (";
+      pr "strcasecmp (cmd, \"%s\") == 0" name;
+      if name <> name2 then
+       pr " || strcasecmp (cmd, \"%s\") == 0" name2;
+      if name <> alias then
+       pr " || strcasecmp (cmd, \"%s\") == 0" alias;
+      pr ")\n";
+      pr "    return run_%s (cmd, argc, argv);\n" name;
+      pr "  else\n";
+  ) all_functions;
+  pr "    {\n";
+  pr "      fprintf (stderr, \"%%s: unknown command\\n\", cmd);\n";
+  pr "      return -1;\n";
+  pr "    }\n";
+  pr "  return 0;\n";
+  pr "}\n";
+  pr "\n"
+
+(* Readline completion for guestfish. *)
+and generate_fish_completion () =
+  generate_header CStyle GPLv2;
+
+  let all_functions =
+    List.filter (
+      fun (_, _, _, flags, _, _, _) -> not (List.mem NotInFish flags)
+    ) all_functions in
+
+  pr "\
+#include <config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#ifdef HAVE_LIBREADLINE
+#include <readline/readline.h>
+#endif
+
+#include \"fish.h\"
+
+#ifdef HAVE_LIBREADLINE
+
+static const char *const commands[] = {
+  BUILTIN_COMMANDS_FOR_COMPLETION,
+";
+
+  (* Get the commands, including the aliases.  They don't need to be
+   * sorted - the generator() function just does a dumb linear search.
+   *)
+  let commands =
+    List.map (
+      fun (name, _, _, flags, _, _, _) ->
+       let name2 = replace_char name '_' '-' in
+       let alias =
+         try find_map (function FishAlias n -> Some n | _ -> None) flags
+         with Not_found -> name in
+
+       if name <> alias then [name2; alias] else [name2]
+    ) all_functions in
+  let commands = List.flatten commands in
+
+  List.iter (pr "  \"%s\",\n") commands;
+
+  pr "  NULL
+};
+
+static char *
+generator (const char *text, int state)
+{
+  static int index, len;
+  const char *name;
+
+  if (!state) {
+    index = 0;
+    len = strlen (text);
+  }
+
+  rl_attempted_completion_over = 1;
+
+  while ((name = commands[index]) != NULL) {
+    index++;
+    if (strncasecmp (name, text, len) == 0)
+      return strdup (name);
+  }
+
+  return NULL;
+}
+
+#endif /* HAVE_LIBREADLINE */
+
+char **do_completion (const char *text, int start, int end)
+{
+  char **matches = NULL;
+
+#ifdef HAVE_LIBREADLINE
+  rl_completion_append_character = ' ';
+
+  if (start == 0)
+    matches = rl_completion_matches (text, generator);
+  else if (complete_dest_paths)
+    matches = rl_completion_matches (text, complete_dest_paths_generator);
+#endif
+
+  return matches;
+}
+";
+
+(* Generate the POD documentation for guestfish. *)
+and generate_fish_actions_pod () =
+  let all_functions_sorted =
+    List.filter (
+      fun (_, _, _, flags, _, _, _) ->
+       not (List.mem NotInFish flags || List.mem NotInDocs flags)
+    ) all_functions_sorted in
+
+  let rex = Str.regexp "C<guestfs_\\([^>]+\\)>" in
+
+  List.iter (
+    fun (name, style, _, flags, _, _, longdesc) ->
+      let longdesc =
+       Str.global_substitute rex (
+         fun s ->
+           let sub =
+             try Str.matched_group 1 s
+             with Not_found ->
+               failwithf "error substituting C<guestfs_...> in longdesc of function %s" name in
+           "C<" ^ replace_char sub '_' '-' ^ ">"
+       ) longdesc in
+      let name = replace_char name '_' '-' in
+      let alias =
+       try find_map (function FishAlias n -> Some n | _ -> None) flags
+       with Not_found -> name in
+
+      pr "=head2 %s" name;
+      if name <> alias then
+       pr " | %s" alias;
+      pr "\n";
+      pr "\n";
+      pr " %s" name;
+      List.iter (
+       function
+       | String n -> pr " %s" n
+       | OptString n -> pr " %s" n
+       | StringList n -> pr " '%s ...'" n
+       | Bool _ -> pr " true|false"
+       | Int n -> pr " %s" n
+       | FileIn n | FileOut n -> pr " (%s|-)" n
+      ) (snd style);
+      pr "\n";
+      pr "\n";
+      pr "%s\n\n" longdesc;
+
+      if List.exists (function FileIn _ | FileOut _ -> true
+                     | _ -> false) (snd style) then
+       pr "Use C<-> instead of a filename to read/write from stdin/stdout.\n\n";
+
+      if List.mem ProtocolLimitWarning flags then
+       pr "%s\n\n" protocol_limit_warning;
+
+      if List.mem DangerWillRobinson flags then
+       pr "%s\n\n" danger_will_robinson
+  ) all_functions_sorted
+
+(* Generate a C function prototype. *)
+and generate_prototype ?(extern = true) ?(static = false) ?(semicolon = true)
+    ?(single_line = false) ?(newline = false) ?(in_daemon = false)
+    ?(prefix = "")
+    ?handle name style =
+  if extern then pr "extern ";
+  if static then pr "static ";
+  (match fst style with
+   | RErr -> pr "int "
+   | RInt _ -> pr "int "
+   | RInt64 _ -> pr "int64_t "
+   | RBool _ -> pr "int "
+   | RConstString _ -> pr "const char *"
+   | RString _ -> pr "char *"
+   | RStringList _ | RHashtable _ -> pr "char **"
+   | RIntBool _ ->
+       if not in_daemon then pr "struct guestfs_int_bool *"
+       else pr "guestfs_%s_ret *" name
+   | RPVList _ ->
+       if not in_daemon then pr "struct guestfs_lvm_pv_list *"
+       else pr "guestfs_lvm_int_pv_list *"
+   | RVGList _ ->
+       if not in_daemon then pr "struct guestfs_lvm_vg_list *"
+       else pr "guestfs_lvm_int_vg_list *"
+   | RLVList _ ->
+       if not in_daemon then pr "struct guestfs_lvm_lv_list *"
+       else pr "guestfs_lvm_int_lv_list *"
+   | RStat _ ->
+       if not in_daemon then pr "struct guestfs_stat *"
+       else pr "guestfs_int_stat *"
+   | RStatVFS _ ->
+       if not in_daemon then pr "struct guestfs_statvfs *"
+       else pr "guestfs_int_statvfs *"
+  );
+  pr "%s%s (" prefix name;
+  if handle = None && List.length (snd style) = 0 then
+    pr "void"
+  else (
+    let comma = ref false in
+    (match handle with
+     | None -> ()
+     | Some handle -> pr "guestfs_h *%s" handle; comma := true
+    );
+    let next () =
+      if !comma then (
+       if single_line then pr ", " else pr ",\n\t\t"
+      );
+      comma := true
+    in
+    List.iter (
+      function
+      | String n
+      | OptString n ->
+         next ();
+         if not in_daemon then pr "const char *%s" n
+         else pr "char *%s" n
+      | StringList n ->
+         next ();
+         if not in_daemon then pr "char * const* const %s" n
+         else pr "char **%s" n
+      | Bool n -> next (); pr "int %s" n
+      | Int n -> next (); pr "int %s" n
+      | FileIn n
+      | FileOut n ->
+         if not in_daemon then (next (); pr "const char *%s" n)
+    ) (snd style);
+  );
+  pr ")";
+  if semicolon then pr ";";
+  if newline then pr "\n"
+
+(* Generate C call arguments, eg "(handle, foo, bar)" *)
+and generate_call_args ?handle args =
+  pr "(";
+  let comma = ref false in
+  (match handle with
+   | None -> ()
+   | Some handle -> pr "%s" handle; comma := true
+  );
+  List.iter (
+    fun arg ->
+      if !comma then pr ", ";
+      comma := true;
+      pr "%s" (name_of_argt arg)
+  ) args;
+  pr ")"
+
+(* Generate the OCaml bindings interface. *)
+and generate_ocaml_mli () =
+  generate_header OCamlStyle LGPLv2;
+
+  pr "\
+(** For API documentation you should refer to the C API
+    in the guestfs(3) manual page.  The OCaml API uses almost
+    exactly the same calls. *)
+
+type t
+(** A [guestfs_h] handle. *)
+
+exception Error of string
+(** This exception is raised when there is an error. *)
+
+val create : unit -> t
+
+val close : t -> unit
+(** Handles are closed by the garbage collector when they become
+    unreferenced, but callers can also call this in order to
+    provide predictable cleanup. *)
+
+";
+  generate_ocaml_lvm_structure_decls ();
+
+  generate_ocaml_stat_structure_decls ();
+
+  (* The actions. *)
+  List.iter (
+    fun (name, style, _, _, _, shortdesc, _) ->
+      generate_ocaml_prototype name style;
+      pr "(** %s *)\n" shortdesc;
+      pr "\n"
+  ) all_functions
+
+(* Generate the OCaml bindings implementation. *)
+and generate_ocaml_ml () =
+  generate_header OCamlStyle LGPLv2;
+
+  pr "\
+type t
+exception Error of string
+external create : unit -> t = \"ocaml_guestfs_create\"
+external close : t -> unit = \"ocaml_guestfs_close\"
+
+let () =
+  Callback.register_exception \"ocaml_guestfs_error\" (Error \"\")
+
+";
+
+  generate_ocaml_lvm_structure_decls ();
+
+  generate_ocaml_stat_structure_decls ();
+
+  (* The actions. *)
+  List.iter (
+    fun (name, style, _, _, _, shortdesc, _) ->
+      generate_ocaml_prototype ~is_external:true name style;
+  ) all_functions
+
+(* Generate the OCaml bindings C implementation. *)
+and generate_ocaml_c () =
+  generate_header CStyle LGPLv2;
+
+  pr "\
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <caml/config.h>
+#include <caml/alloc.h>
+#include <caml/callback.h>
+#include <caml/fail.h>
+#include <caml/memory.h>
+#include <caml/mlvalues.h>
+#include <caml/signals.h>
+
+#include <guestfs.h>
+
+#include \"guestfs_c.h\"
+
+/* Copy a hashtable of string pairs into an assoc-list.  We return
+ * the list in reverse order, but hashtables aren't supposed to be
+ * ordered anyway.
+ */
+static CAMLprim value
+copy_table (char * const * argv)
+{
+  CAMLparam0 ();
+  CAMLlocal5 (rv, pairv, kv, vv, cons);
+  int i;
+
+  rv = Val_int (0);
+  for (i = 0; argv[i] != NULL; i += 2) {
+    kv = caml_copy_string (argv[i]);
+    vv = caml_copy_string (argv[i+1]);
+    pairv = caml_alloc (2, 0);
+    Store_field (pairv, 0, kv);
+    Store_field (pairv, 1, vv);
+    cons = caml_alloc (2, 0);
+    Store_field (cons, 1, rv);
+    rv = cons;
+    Store_field (cons, 0, pairv);
+  }
+
+  CAMLreturn (rv);
+}
+
+";
+
+  (* LVM struct copy functions. *)
+  List.iter (
+    fun (typ, cols) ->
+      let has_optpercent_col =
+       List.exists (function (_, `OptPercent) -> true | _ -> false) cols in
+
+      pr "static CAMLprim value\n";
+      pr "copy_lvm_%s (const struct guestfs_lvm_%s *%s)\n" typ typ typ;
+      pr "{\n";
+      pr "  CAMLparam0 ();\n";
+      if has_optpercent_col then
+       pr "  CAMLlocal3 (rv, v, v2);\n"
+      else
+       pr "  CAMLlocal2 (rv, v);\n";
+      pr "\n";
+      pr "  rv = caml_alloc (%d, 0);\n" (List.length cols);
+      iteri (
+       fun i col ->
+         (match col with
+          | name, `String ->
+              pr "  v = caml_copy_string (%s->%s);\n" typ name
+          | name, `UUID ->
+              pr "  v = caml_alloc_string (32);\n";
+              pr "  memcpy (String_val (v), %s->%s, 32);\n" typ name
+          | name, `Bytes
+          | name, `Int ->
+              pr "  v = caml_copy_int64 (%s->%s);\n" typ name
+          | name, `OptPercent ->
+              pr "  if (%s->%s >= 0) { /* Some %s */\n" typ name name;
+              pr "    v2 = caml_copy_double (%s->%s);\n" typ name;
+              pr "    v = caml_alloc (1, 0);\n";
+              pr "    Store_field (v, 0, v2);\n";
+              pr "  } else /* None */\n";
+              pr "    v = Val_int (0);\n";
+         );
+         pr "  Store_field (rv, %d, v);\n" i
+      ) cols;
+      pr "  CAMLreturn (rv);\n";
+      pr "}\n";
+      pr "\n";
+
+      pr "static CAMLprim value\n";
+      pr "copy_lvm_%s_list (const struct guestfs_lvm_%s_list *%ss)\n"
+       typ typ typ;
+      pr "{\n";
+      pr "  CAMLparam0 ();\n";
+      pr "  CAMLlocal2 (rv, v);\n";
+      pr "  int i;\n";
+      pr "\n";
+      pr "  if (%ss->len == 0)\n" typ;
+      pr "    CAMLreturn (Atom (0));\n";
+      pr "  else {\n";
+      pr "    rv = caml_alloc (%ss->len, 0);\n" typ;
+      pr "    for (i = 0; i < %ss->len; ++i) {\n" typ;
+      pr "      v = copy_lvm_%s (&%ss->val[i]);\n" typ typ;
+      pr "      caml_modify (&Field (rv, i), v);\n";
+      pr "    }\n";
+      pr "    CAMLreturn (rv);\n";
+      pr "  }\n";
+      pr "}\n";
+      pr "\n";
+  ) ["pv", pv_cols; "vg", vg_cols; "lv", lv_cols];
+
+  (* Stat copy functions. *)
+  List.iter (
+    fun (typ, cols) ->
+      pr "static CAMLprim value\n";
+      pr "copy_%s (const struct guestfs_%s *%s)\n" typ typ typ;
+      pr "{\n";
+      pr "  CAMLparam0 ();\n";
+      pr "  CAMLlocal2 (rv, v);\n";
+      pr "\n";
+      pr "  rv = caml_alloc (%d, 0);\n" (List.length cols);
+      iteri (
+       fun i col ->
+         (match col with
+          | name, `Int ->
+              pr "  v = caml_copy_int64 (%s->%s);\n" typ name
+         );
+         pr "  Store_field (rv, %d, v);\n" i
+      ) cols;
+      pr "  CAMLreturn (rv);\n";
+      pr "}\n";
+      pr "\n";
+  ) ["stat", stat_cols; "statvfs", statvfs_cols];
+
+  (* The wrappers. *)
+  List.iter (
+    fun (name, style, _, _, _, _, _) ->
+      let params =
+       "gv" :: List.map (fun arg -> name_of_argt arg ^ "v") (snd style) in
+
+      pr "CAMLprim value\n";
+      pr "ocaml_guestfs_%s (value %s" name (List.hd params);
+      List.iter (pr ", value %s") (List.tl params);
+      pr ")\n";
+      pr "{\n";
+
+      (match params with
+       | [p1; p2; p3; p4; p5] ->
+          pr "  CAMLparam5 (%s);\n" (String.concat ", " params)
+       | p1 :: p2 :: p3 :: p4 :: p5 :: rest ->
+          pr "  CAMLparam5 (%s);\n" (String.concat ", " [p1; p2; p3; p4; p5]);
+          pr "  CAMLxparam%d (%s);\n"
+            (List.length rest) (String.concat ", " rest)
+       | ps ->
+          pr "  CAMLparam%d (%s);\n" (List.length ps) (String.concat ", " ps)
+      );
+      pr "  CAMLlocal1 (rv);\n";
+      pr "\n";
+
+      pr "  guestfs_h *g = Guestfs_val (gv);\n";
+      pr "  if (g == NULL)\n";
+      pr "    caml_failwith (\"%s: used handle after closing it\");\n" name;
+      pr "\n";
+
+      List.iter (
+       function
+       | String n
+       | FileIn n
+       | FileOut n ->
+           pr "  const char *%s = String_val (%sv);\n" n n
+       | OptString n ->
+           pr "  const char *%s =\n" n;
+           pr "    %sv != Val_int (0) ? String_val (Field (%sv, 0)) : NULL;\n"
+             n n
+       | StringList n ->
+           pr "  char **%s = ocaml_guestfs_strings_val (g, %sv);\n" n n
+       | Bool n ->
+           pr "  int %s = Bool_val (%sv);\n" n n
+       | Int n ->
+           pr "  int %s = Int_val (%sv);\n" n n
+      ) (snd style);
+      let error_code =
+       match fst style with
+       | RErr -> pr "  int r;\n"; "-1"
+       | RInt _ -> pr "  int r;\n"; "-1"
+       | RInt64 _ -> pr "  int64_t r;\n"; "-1"
+       | RBool _ -> pr "  int r;\n"; "-1"
+       | RConstString _ -> pr "  const char *r;\n"; "NULL"
+       | RString _ -> pr "  char *r;\n"; "NULL"
+       | RStringList _ ->
+           pr "  int i;\n";
+           pr "  char **r;\n";
+           "NULL"
+       | RIntBool _ ->
+           pr "  struct guestfs_int_bool *r;\n"; "NULL"
+       | RPVList _ ->
+           pr "  struct guestfs_lvm_pv_list *r;\n"; "NULL"
+       | RVGList _ ->
+           pr "  struct guestfs_lvm_vg_list *r;\n"; "NULL"
+       | RLVList _ ->
+           pr "  struct guestfs_lvm_lv_list *r;\n"; "NULL"
+       | RStat _ ->
+           pr "  struct guestfs_stat *r;\n"; "NULL"
+       | RStatVFS _ ->
+           pr "  struct guestfs_statvfs *r;\n"; "NULL"
+       | RHashtable _ ->
+           pr "  int i;\n";
+           pr "  char **r;\n";
+           "NULL" in
+      pr "\n";
+
+      pr "  caml_enter_blocking_section ();\n";
+      pr "  r = guestfs_%s " name;
+      generate_call_args ~handle:"g" (snd style);
+      pr ";\n";
+      pr "  caml_leave_blocking_section ();\n";
+
+      List.iter (
+       function
+       | StringList n ->
+           pr "  ocaml_guestfs_free_strings (%s);\n" n;
+       | String _ | OptString _ | Bool _ | Int _ | FileIn _ | FileOut _ -> ()
+      ) (snd style);
+
+      pr "  if (r == %s)\n" error_code;
+      pr "    ocaml_guestfs_raise_error (g, \"%s\");\n" name;
+      pr "\n";
+
+      (match fst style with
+       | RErr -> pr "  rv = Val_unit;\n"
+       | RInt _ -> pr "  rv = Val_int (r);\n"
+       | RInt64 _ ->
+          pr "  rv = caml_copy_int64 (r);\n"
+       | RBool _ -> pr "  rv = Val_bool (r);\n"
+       | RConstString _ -> pr "  rv = caml_copy_string (r);\n"
+       | RString _ ->
+          pr "  rv = caml_copy_string (r);\n";
+          pr "  free (r);\n"
+       | RStringList _ ->
+          pr "  rv = caml_copy_string_array ((const char **) r);\n";
+          pr "  for (i = 0; r[i] != NULL; ++i) free (r[i]);\n";
+          pr "  free (r);\n"
+       | RIntBool _ ->
+          pr "  rv = caml_alloc (2, 0);\n";
+          pr "  Store_field (rv, 0, Val_int (r->i));\n";
+          pr "  Store_field (rv, 1, Val_bool (r->b));\n";
+          pr "  guestfs_free_int_bool (r);\n";
+       | RPVList _ ->
+          pr "  rv = copy_lvm_pv_list (r);\n";
+          pr "  guestfs_free_lvm_pv_list (r);\n";
+       | RVGList _ ->
+          pr "  rv = copy_lvm_vg_list (r);\n";
+          pr "  guestfs_free_lvm_vg_list (r);\n";
+       | RLVList _ ->
+          pr "  rv = copy_lvm_lv_list (r);\n";
+          pr "  guestfs_free_lvm_lv_list (r);\n";
+       | RStat _ ->
+          pr "  rv = copy_stat (r);\n";
+          pr "  free (r);\n";
+       | RStatVFS _ ->
+          pr "  rv = copy_statvfs (r);\n";
+          pr "  free (r);\n";
+       | RHashtable _ ->
+          pr "  rv = copy_table (r);\n";
+          pr "  for (i = 0; r[i] != NULL; ++i) free (r[i]);\n";
+          pr "  free (r);\n";
+      );
+
+      pr "  CAMLreturn (rv);\n";
+      pr "}\n";
+      pr "\n";
+
+      if List.length params > 5 then (
+       pr "CAMLprim value\n";
+       pr "ocaml_guestfs_%s_byte (value *argv, int argn)\n" name;
+       pr "{\n";
+       pr "  return ocaml_guestfs_%s (argv[0]" name;
+       iteri (fun i _ -> pr ", argv[%d]" i) (List.tl params);
+       pr ");\n";
+       pr "}\n";
+       pr "\n"
+      )
+  ) all_functions
+
+and generate_ocaml_lvm_structure_decls () =
+  List.iter (
+    fun (typ, cols) ->
+      pr "type lvm_%s = {\n" typ;
+      List.iter (
+       function
+       | name, `String -> pr "  %s : string;\n" name
+       | name, `UUID -> pr "  %s : string;\n" name
+       | name, `Bytes -> pr "  %s : int64;\n" name
+       | name, `Int -> pr "  %s : int64;\n" name
+       | name, `OptPercent -> pr "  %s : float option;\n" name
+      ) cols;
+      pr "}\n";
+      pr "\n"
+  ) ["pv", pv_cols; "vg", vg_cols; "lv", lv_cols]
+
+and generate_ocaml_stat_structure_decls () =
+  List.iter (
+    fun (typ, cols) ->
+      pr "type %s = {\n" typ;
+      List.iter (
+       function
+       | name, `Int -> pr "  %s : int64;\n" name
+      ) cols;
+      pr "}\n";
+      pr "\n"
+  ) ["stat", stat_cols; "statvfs", statvfs_cols]
+
+and generate_ocaml_prototype ?(is_external = false) name style =
+  if is_external then pr "external " else pr "val ";
+  pr "%s : t -> " name;
+  List.iter (
+    function
+    | String _ | FileIn _ | FileOut _ -> pr "string -> "
+    | OptString _ -> pr "string option -> "
+    | StringList _ -> pr "string array -> "
+    | Bool _ -> pr "bool -> "
+    | Int _ -> pr "int -> "
+  ) (snd style);
+  (match fst style with
+   | RErr -> pr "unit" (* all errors are turned into exceptions *)
+   | RInt _ -> pr "int"
+   | RInt64 _ -> pr "int64"
+   | RBool _ -> pr "bool"
+   | RConstString _ -> pr "string"
+   | RString _ -> pr "string"
+   | RStringList _ -> pr "string array"
+   | RIntBool _ -> pr "int * bool"
+   | RPVList _ -> pr "lvm_pv array"
+   | RVGList _ -> pr "lvm_vg array"
+   | RLVList _ -> pr "lvm_lv array"
+   | RStat _ -> pr "stat"
+   | RStatVFS _ -> pr "statvfs"
+   | RHashtable _ -> pr "(string * string) list"
+  );
+  if is_external then (
+    pr " = ";
+    if List.length (snd style) + 1 > 5 then
+      pr "\"ocaml_guestfs_%s_byte\" " name;
+    pr "\"ocaml_guestfs_%s\"" name
+  );
+  pr "\n"
+
+(* Generate Perl xs code, a sort of crazy variation of C with macros. *)
+and generate_perl_xs () =
+  generate_header CStyle LGPLv2;
+
+  pr "\
+#include \"EXTERN.h\"
+#include \"perl.h\"
+#include \"XSUB.h\"
+
+#include <guestfs.h>
+
+#ifndef PRId64
+#define PRId64 \"lld\"
+#endif
+
+static SV *
+my_newSVll(long long val) {
+#ifdef USE_64_BIT_ALL
+  return newSViv(val);
+#else
+  char buf[100];
+  int len;
+  len = snprintf(buf, 100, \"%%\" PRId64, val);
+  return newSVpv(buf, len);
+#endif
+}
+
+#ifndef PRIu64
+#define PRIu64 \"llu\"
+#endif
+
+static SV *
+my_newSVull(unsigned long long val) {
+#ifdef USE_64_BIT_ALL
+  return newSVuv(val);
+#else
+  char buf[100];
+  int len;
+  len = snprintf(buf, 100, \"%%\" PRIu64, val);
+  return newSVpv(buf, len);
+#endif
+}
+
+/* http://www.perlmonks.org/?node_id=680842 */
+static char **
+XS_unpack_charPtrPtr (SV *arg) {
+  char **ret;
+  AV *av;
+  I32 i;
+
+  if (!arg || !SvOK (arg) || !SvROK (arg) || SvTYPE (SvRV (arg)) != SVt_PVAV)
+    croak (\"array reference expected\");
+
+  av = (AV *)SvRV (arg);
+  ret = malloc ((av_len (av) + 1 + 1) * sizeof (char *));
+  if (!ret)
+    croak (\"malloc failed\");
+
+  for (i = 0; i <= av_len (av); i++) {
+    SV **elem = av_fetch (av, i, 0);
+
+    if (!elem || !*elem)
+      croak (\"missing element in list\");
+
+    ret[i] = SvPV_nolen (*elem);
+  }
+
+  ret[i] = NULL;
+
+  return ret;
+}
+
+MODULE = Sys::Guestfs  PACKAGE = Sys::Guestfs
+
+PROTOTYPES: ENABLE
+
+guestfs_h *
+_create ()
+   CODE:
+      RETVAL = guestfs_create ();
+      if (!RETVAL)
+        croak (\"could not create guestfs handle\");
+      guestfs_set_error_handler (RETVAL, NULL, NULL);
+ OUTPUT:
+      RETVAL
+
+void
+DESTROY (g)
+      guestfs_h *g;
+ PPCODE:
+      guestfs_close (g);
+
+";
+
+  List.iter (
+    fun (name, style, _, _, _, _, _) ->
+      (match fst style with
+       | RErr -> pr "void\n"
+       | RInt _ -> pr "SV *\n"
+       | RInt64 _ -> pr "SV *\n"
+       | RBool _ -> pr "SV *\n"
+       | RConstString _ -> pr "SV *\n"
+       | RString _ -> pr "SV *\n"
+       | RStringList _
+       | RIntBool _
+       | RPVList _ | RVGList _ | RLVList _
+       | RStat _ | RStatVFS _
+       | RHashtable _ ->
+          pr "void\n" (* all lists returned implictly on the stack *)
+      );
+      (* Call and arguments. *)
+      pr "%s " name;
+      generate_call_args ~handle:"g" (snd style);
+      pr "\n";
+      pr "      guestfs_h *g;\n";
+      iteri (
+       fun i ->
+         function
+         | String n | FileIn n | FileOut n -> pr "      char *%s;\n" n
+         | OptString n ->
+             (* http://www.perlmonks.org/?node_id=554277
+              * Note that the implicit handle argument means we have
+              * to add 1 to the ST(x) operator.
+              *)
+             pr "      char *%s = SvOK(ST(%d)) ? SvPV_nolen(ST(%d)) : NULL;\n" n (i+1) (i+1)
+         | StringList n -> pr "      char **%s;\n" n
+         | Bool n -> pr "      int %s;\n" n
+         | Int n -> pr "      int %s;\n" n
+      ) (snd style);
+
+      let do_cleanups () =
+       List.iter (
+         function
+         | String _ | OptString _ | Bool _ | Int _
+         | FileIn _ | FileOut _ -> ()
+         | StringList n -> pr "      free (%s);\n" n
+       ) (snd style)
+      in
+
+      (* Code. *)
+      (match fst style with
+       | RErr ->
+          pr "PREINIT:\n";
+          pr "      int r;\n";
+          pr " PPCODE:\n";
+          pr "      r = guestfs_%s " name;
+          generate_call_args ~handle:"g" (snd style);
+          pr ";\n";
+          do_cleanups ();
+          pr "      if (r == -1)\n";
+          pr "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
+       | RInt n
+       | RBool n ->
+          pr "PREINIT:\n";
+          pr "      int %s;\n" n;
+          pr "   CODE:\n";
+          pr "      %s = guestfs_%s " n name;
+          generate_call_args ~handle:"g" (snd style);
+          pr ";\n";
+          do_cleanups ();
+          pr "      if (%s == -1)\n" n;
+          pr "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
+          pr "      RETVAL = newSViv (%s);\n" n;
+          pr " OUTPUT:\n";
+          pr "      RETVAL\n"
+       | RInt64 n ->
+          pr "PREINIT:\n";
+          pr "      int64_t %s;\n" n;
+          pr "   CODE:\n";
+          pr "      %s = guestfs_%s " n name;
+          generate_call_args ~handle:"g" (snd style);
+          pr ";\n";
+          do_cleanups ();
+          pr "      if (%s == -1)\n" n;
+          pr "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
+          pr "      RETVAL = my_newSVll (%s);\n" n;
+          pr " OUTPUT:\n";
+          pr "      RETVAL\n"
+       | RConstString n ->
+          pr "PREINIT:\n";
+          pr "      const char *%s;\n" n;
+          pr "   CODE:\n";
+          pr "      %s = guestfs_%s " n name;
+          generate_call_args ~handle:"g" (snd style);
+          pr ";\n";
+          do_cleanups ();
+          pr "      if (%s == NULL)\n" n;
+          pr "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
+          pr "      RETVAL = newSVpv (%s, 0);\n" n;
+          pr " OUTPUT:\n";
+          pr "      RETVAL\n"
+       | RString n ->
+          pr "PREINIT:\n";
+          pr "      char *%s;\n" n;
+          pr "   CODE:\n";
+          pr "      %s = guestfs_%s " n name;
+          generate_call_args ~handle:"g" (snd style);
+          pr ";\n";
+          do_cleanups ();
+          pr "      if (%s == NULL)\n" n;
+          pr "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
+          pr "      RETVAL = newSVpv (%s, 0);\n" n;
+          pr "      free (%s);\n" n;
+          pr " OUTPUT:\n";
+          pr "      RETVAL\n"
+       | RStringList n | RHashtable n ->
+          pr "PREINIT:\n";
+          pr "      char **%s;\n" n;
+          pr "      int i, n;\n";
+          pr " PPCODE:\n";
+          pr "      %s = guestfs_%s " n name;
+          generate_call_args ~handle:"g" (snd style);
+          pr ";\n";
+          do_cleanups ();
+          pr "      if (%s == NULL)\n" n;
+          pr "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
+          pr "      for (n = 0; %s[n] != NULL; ++n) /**/;\n" n;
+          pr "      EXTEND (SP, n);\n";
+          pr "      for (i = 0; i < n; ++i) {\n";
+          pr "        PUSHs (sv_2mortal (newSVpv (%s[i], 0)));\n" n;
+          pr "        free (%s[i]);\n" n;
+          pr "      }\n";
+          pr "      free (%s);\n" n;
+       | RIntBool _ ->
+          pr "PREINIT:\n";
+          pr "      struct guestfs_int_bool *r;\n";
+          pr " PPCODE:\n";
+          pr "      r = guestfs_%s " name;
+          generate_call_args ~handle:"g" (snd style);
+          pr ";\n";
+          do_cleanups ();
+          pr "      if (r == NULL)\n";
+          pr "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
+          pr "      EXTEND (SP, 2);\n";
+          pr "      PUSHs (sv_2mortal (newSViv (r->i)));\n";
+          pr "      PUSHs (sv_2mortal (newSViv (r->b)));\n";
+          pr "      guestfs_free_int_bool (r);\n";
+       | RPVList n ->
+          generate_perl_lvm_code "pv" pv_cols name style n do_cleanups
+       | RVGList n ->
+          generate_perl_lvm_code "vg" vg_cols name style n do_cleanups
+       | RLVList n ->
+          generate_perl_lvm_code "lv" lv_cols name style n do_cleanups
+       | RStat n ->
+          generate_perl_stat_code "stat" stat_cols name style n do_cleanups
+       | RStatVFS n ->
+          generate_perl_stat_code
+            "statvfs" statvfs_cols name style n do_cleanups
+      );
+
+      pr "\n"
+  ) all_functions
+
+and generate_perl_lvm_code typ cols name style n do_cleanups =
+  pr "PREINIT:\n";
+  pr "      struct guestfs_lvm_%s_list *%s;\n" typ n;
+  pr "      int i;\n";
+  pr "      HV *hv;\n";
+  pr " PPCODE:\n";
+  pr "      %s = guestfs_%s " n name;
+  generate_call_args ~handle:"g" (snd style);
+  pr ";\n";
+  do_cleanups ();
+  pr "      if (%s == NULL)\n" n;
+  pr "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
+  pr "      EXTEND (SP, %s->len);\n" n;
+  pr "      for (i = 0; i < %s->len; ++i) {\n" n;
+  pr "        hv = newHV ();\n";
+  List.iter (
+    function
+    | name, `String ->
+       pr "        (void) hv_store (hv, \"%s\", %d, newSVpv (%s->val[i].%s, 0), 0);\n"
+         name (String.length name) n name
+    | name, `UUID ->
+       pr "        (void) hv_store (hv, \"%s\", %d, newSVpv (%s->val[i].%s, 32), 0);\n"
+         name (String.length name) n name
+    | name, `Bytes ->
+       pr "        (void) hv_store (hv, \"%s\", %d, my_newSVull (%s->val[i].%s), 0);\n"
+         name (String.length name) n name
+    | name, `Int ->
+       pr "        (void) hv_store (hv, \"%s\", %d, my_newSVll (%s->val[i].%s), 0);\n"
+         name (String.length name) n name
+    | name, `OptPercent ->
+       pr "        (void) hv_store (hv, \"%s\", %d, newSVnv (%s->val[i].%s), 0);\n"
+         name (String.length name) n name
+  ) cols;
+  pr "        PUSHs (sv_2mortal ((SV *) hv));\n";
+  pr "      }\n";
+  pr "      guestfs_free_lvm_%s_list (%s);\n" typ n
+
+and generate_perl_stat_code typ cols name style n do_cleanups =
+  pr "PREINIT:\n";
+  pr "      struct guestfs_%s *%s;\n" typ n;
+  pr " PPCODE:\n";
+  pr "      %s = guestfs_%s " n name;
+  generate_call_args ~handle:"g" (snd style);
+  pr ";\n";
+  do_cleanups ();
+  pr "      if (%s == NULL)\n" n;
+  pr "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
+  pr "      EXTEND (SP, %d);\n" (List.length cols);
+  List.iter (
+    function
+    | name, `Int ->
+       pr "      PUSHs (sv_2mortal (my_newSVll (%s->%s)));\n" n name
+  ) cols;
+  pr "      free (%s);\n" n
+
+(* Generate Sys/Guestfs.pm. *)
+and generate_perl_pm () =
+  generate_header HashStyle LGPLv2;
+
+  pr "\
+=pod
+
+=head1 NAME
+
+Sys::Guestfs - Perl bindings for libguestfs
+
+=head1 SYNOPSIS
+
+ use Sys::Guestfs;
+ my $h = Sys::Guestfs->new ();
+ $h->add_drive ('guest.img');
+ $h->launch ();
+ $h->wait_ready ();
+ $h->mount ('/dev/sda1', '/');
+ $h->touch ('/hello');
+ $h->sync ();
+
+=head1 DESCRIPTION
 
-      List.iter (
-       function
-       | String n
-       | FileIn n
-       | FileOut n ->
-           pr "  const char *%s = String_val (%sv);\n" n n
-       | OptString n ->
-           pr "  const char *%s =\n" n;
-           pr "    %sv != Val_int (0) ? String_val (Field (%sv, 0)) : NULL;\n"
-             n n
-       | StringList n ->
-           pr "  char **%s = ocaml_guestfs_strings_val (%sv);\n" n n
-       | Bool n ->
-           pr "  int %s = Bool_val (%sv);\n" n n
-       | Int n ->
-           pr "  int %s = Int_val (%sv);\n" n n
-      ) (snd style);
-      let error_code =
-       match fst style with
-       | RErr -> pr "  int r;\n"; "-1"
-       | RInt _ -> pr "  int r;\n"; "-1"
-       | RInt64 _ -> pr "  int64_t r;\n"; "-1"
-       | RBool _ -> pr "  int r;\n"; "-1"
-       | RConstString _ -> pr "  const char *r;\n"; "NULL"
-       | RString _ -> pr "  char *r;\n"; "NULL"
-       | RStringList _ ->
-           pr "  int i;\n";
-           pr "  char **r;\n";
-           "NULL"
-       | RIntBool _ ->
-           pr "  struct guestfs_int_bool *r;\n"; "NULL"
-       | RPVList _ ->
-           pr "  struct guestfs_lvm_pv_list *r;\n"; "NULL"
-       | RVGList _ ->
-           pr "  struct guestfs_lvm_vg_list *r;\n"; "NULL"
-       | RLVList _ ->
-           pr "  struct guestfs_lvm_lv_list *r;\n"; "NULL"
-       | RStat _ ->
-           pr "  struct guestfs_stat *r;\n"; "NULL"
-       | RStatVFS _ ->
-           pr "  struct guestfs_statvfs *r;\n"; "NULL"
-       | RHashtable _ ->
-           pr "  int i;\n";
-           pr "  char **r;\n";
-           "NULL" in
-      pr "\n";
+The C<Sys::Guestfs> module provides a Perl XS binding to the
+libguestfs API for examining and modifying virtual machine
+disk images.
 
-      pr "  caml_enter_blocking_section ();\n";
-      pr "  r = guestfs_%s " name;
-      generate_call_args ~handle:"g" (snd style);
-      pr ";\n";
-      pr "  caml_leave_blocking_section ();\n";
+Amongst the things this is good for: making batch configuration
+changes to guests, getting disk used/free statistics (see also:
+virt-df), migrating between virtualization systems (see also:
+virt-p2v), performing partial backups, performing partial guest
+clones, cloning guests and changing registry/UUID/hostname info, and
+much else besides.
 
-      List.iter (
-       function
-       | StringList n ->
-           pr "  ocaml_guestfs_free_strings (%s);\n" n;
-       | String _ | OptString _ | Bool _ | Int _ | FileIn _ | FileOut _ -> ()
-      ) (snd style);
+Libguestfs uses Linux kernel and qemu code, and can access any type of
+guest filesystem that Linux and qemu can, including but not limited
+to: ext2/3/4, btrfs, FAT and NTFS, LVM, many different disk partition
+schemes, qcow, qcow2, vmdk.
 
-      pr "  if (r == %s)\n" error_code;
-      pr "    ocaml_guestfs_raise_error (g, \"%s\");\n" name;
-      pr "\n";
+Libguestfs provides ways to enumerate guest storage (eg. partitions,
+LVs, what filesystem is in each LV, etc.).  It can also run commands
+in the context of the guest.  Also you can access filesystems over FTP.
 
-      (match fst style with
-       | RErr -> pr "  rv = Val_unit;\n"
-       | RInt _ -> pr "  rv = Val_int (r);\n"
-       | RInt64 _ ->
-          pr "  rv = caml_copy_int64 (r);\n"
-       | RBool _ -> pr "  rv = Val_bool (r);\n"
-       | RConstString _ -> pr "  rv = caml_copy_string (r);\n"
-       | RString _ ->
-          pr "  rv = caml_copy_string (r);\n";
-          pr "  free (r);\n"
-       | RStringList _ ->
-          pr "  rv = caml_copy_string_array ((const char **) r);\n";
-          pr "  for (i = 0; r[i] != NULL; ++i) free (r[i]);\n";
-          pr "  free (r);\n"
-       | RIntBool _ ->
-          pr "  rv = caml_alloc (2, 0);\n";
-          pr "  Store_field (rv, 0, Val_int (r->i));\n";
-          pr "  Store_field (rv, 1, Val_bool (r->b));\n";
-          pr "  guestfs_free_int_bool (r);\n";
-       | RPVList _ ->
-          pr "  rv = copy_lvm_pv_list (r);\n";
-          pr "  guestfs_free_lvm_pv_list (r);\n";
-       | RVGList _ ->
-          pr "  rv = copy_lvm_vg_list (r);\n";
-          pr "  guestfs_free_lvm_vg_list (r);\n";
-       | RLVList _ ->
-          pr "  rv = copy_lvm_lv_list (r);\n";
-          pr "  guestfs_free_lvm_lv_list (r);\n";
-       | RStat _ ->
-          pr "  rv = copy_stat (r);\n";
-          pr "  free (r);\n";
-       | RStatVFS _ ->
-          pr "  rv = copy_statvfs (r);\n";
-          pr "  free (r);\n";
-       | RHashtable _ ->
-          pr "  rv = copy_table (r);\n";
-          pr "  for (i = 0; r[i] != NULL; ++i) free (r[i]);\n";
-          pr "  free (r);\n";
-      );
+=head1 ERRORS
+
+All errors turn into calls to C<croak> (see L<Carp(3)>).
+
+=head1 METHODS
+
+=over 4
+
+=cut
+
+package Sys::Guestfs;
+
+use strict;
+use warnings;
+
+require XSLoader;
+XSLoader::load ('Sys::Guestfs');
+
+=item $h = Sys::Guestfs->new ();
+
+Create a new guestfs handle.
+
+=cut
+
+sub new {
+  my $proto = shift;
+  my $class = ref ($proto) || $proto;
+
+  my $self = Sys::Guestfs::_create ();
+  bless $self, $class;
+  return $self;
+}
+
+";
+
+  (* Actions.  We only need to print documentation for these as
+   * they are pulled in from the XS code automatically.
+   *)
+  List.iter (
+    fun (name, style, _, flags, _, _, longdesc) ->
+      if not (List.mem NotInDocs flags) then (
+       let longdesc = replace_str longdesc "C<guestfs_" "C<$h-E<gt>" in
+       pr "=item ";
+       generate_perl_prototype name style;
+       pr "\n\n";
+       pr "%s\n\n" longdesc;
+       if List.mem ProtocolLimitWarning flags then
+         pr "%s\n\n" protocol_limit_warning;
+       if List.mem DangerWillRobinson flags then
+         pr "%s\n\n" danger_will_robinson
+      )
+  ) all_functions_sorted;
+
+  (* End of file. *)
+  pr "\
+=cut
+
+1;
+
+=back
+
+=head1 COPYRIGHT
+
+Copyright (C) 2009 Red Hat Inc.
+
+=head1 LICENSE
+
+Please see the file COPYING.LIB for the full license.
+
+=head1 SEE ALSO
+
+L<guestfs(3)>, L<guestfish(1)>.
+
+=cut
+"
+
+and generate_perl_prototype name style =
+  (match fst style with
+   | RErr -> ()
+   | RBool n
+   | RInt n
+   | RInt64 n
+   | RConstString n
+   | RString n -> pr "$%s = " n
+   | RIntBool (n, m) -> pr "($%s, $%s) = " n m
+   | RStringList n
+   | RPVList n
+   | RVGList n
+   | RLVList n -> pr "@%s = " n
+   | RStat n
+   | RStatVFS n
+   | RHashtable n -> pr "%%%s = " n
+  );
+  pr "$h->%s (" name;
+  let comma = ref false in
+  List.iter (
+    fun arg ->
+      if !comma then pr ", ";
+      comma := true;
+      match arg with
+      | String n | OptString n | Bool n | Int n | FileIn n | FileOut n ->
+         pr "$%s" n
+      | StringList n ->
+         pr "\\@%s" n
+  ) (snd style);
+  pr ");"
+
+(* Generate Python C module. *)
+and generate_python_c () =
+  generate_header CStyle LGPLv2;
+
+  pr "\
+#include <stdio.h>
+#include <stdlib.h>
+#include <assert.h>
+
+#include <Python.h>
+
+#include \"guestfs.h\"
+
+typedef struct {
+  PyObject_HEAD
+  guestfs_h *g;
+} Pyguestfs_Object;
+
+static guestfs_h *
+get_handle (PyObject *obj)
+{
+  assert (obj);
+  assert (obj != Py_None);
+  return ((Pyguestfs_Object *) obj)->g;
+}
+
+static PyObject *
+put_handle (guestfs_h *g)
+{
+  assert (g);
+  return
+    PyCObject_FromVoidPtrAndDesc ((void *) g, (char *) \"guestfs_h\", NULL);
+}
+
+/* This list should be freed (but not the strings) after use. */
+static const char **
+get_string_list (PyObject *obj)
+{
+  int i, len;
+  const char **r;
+
+  assert (obj);
+
+  if (!PyList_Check (obj)) {
+    PyErr_SetString (PyExc_RuntimeError, \"expecting a list parameter\");
+    return NULL;
+  }
 
-      pr "  CAMLreturn (rv);\n";
-      pr "}\n";
-      pr "\n";
+  len = PyList_Size (obj);
+  r = malloc (sizeof (char *) * (len+1));
+  if (r == NULL) {
+    PyErr_SetString (PyExc_RuntimeError, \"get_string_list: out of memory\");
+    return NULL;
+  }
 
-      if List.length params > 5 then (
-       pr "CAMLprim value\n";
-       pr "ocaml_guestfs_%s_byte (value *argv, int argn)\n" name;
-       pr "{\n";
-       pr "  return ocaml_guestfs_%s (argv[0]" name;
-       iteri (fun i _ -> pr ", argv[%d]" i) (List.tl params);
-       pr ");\n";
-       pr "}\n";
-       pr "\n"
-      )
-  ) all_functions
+  for (i = 0; i < len; ++i)
+    r[i] = PyString_AsString (PyList_GetItem (obj, i));
+  r[len] = NULL;
 
-and generate_ocaml_lvm_structure_decls () =
-  List.iter (
-    fun (typ, cols) ->
-      pr "type lvm_%s = {\n" typ;
-      List.iter (
-       function
-       | name, `String -> pr "  %s : string;\n" name
-       | name, `UUID -> pr "  %s : string;\n" name
-       | name, `Bytes -> pr "  %s : int64;\n" name
-       | name, `Int -> pr "  %s : int64;\n" name
-       | name, `OptPercent -> pr "  %s : float option;\n" name
-      ) cols;
-      pr "}\n";
-      pr "\n"
-  ) ["pv", pv_cols; "vg", vg_cols; "lv", lv_cols]
+  return r;
+}
 
-and generate_ocaml_stat_structure_decls () =
-  List.iter (
-    fun (typ, cols) ->
-      pr "type %s = {\n" typ;
-      List.iter (
-       function
-       | name, `Int -> pr "  %s : int64;\n" name
-      ) cols;
-      pr "}\n";
-      pr "\n"
-  ) ["stat", stat_cols; "statvfs", statvfs_cols]
+static PyObject *
+put_string_list (char * const * const argv)
+{
+  PyObject *list;
+  int argc, i;
 
-and generate_ocaml_prototype ?(is_external = false) name style =
-  if is_external then pr "external " else pr "val ";
-  pr "%s : t -> " name;
-  List.iter (
-    function
-    | String _ | FileIn _ | FileOut _ -> pr "string -> "
-    | OptString _ -> pr "string option -> "
-    | StringList _ -> pr "string array -> "
-    | Bool _ -> pr "bool -> "
-    | Int _ -> pr "int -> "
-  ) (snd style);
-  (match fst style with
-   | RErr -> pr "unit" (* all errors are turned into exceptions *)
-   | RInt _ -> pr "int"
-   | RInt64 _ -> pr "int64"
-   | RBool _ -> pr "bool"
-   | RConstString _ -> pr "string"
-   | RString _ -> pr "string"
-   | RStringList _ -> pr "string array"
-   | RIntBool _ -> pr "int * bool"
-   | RPVList _ -> pr "lvm_pv array"
-   | RVGList _ -> pr "lvm_vg array"
-   | RLVList _ -> pr "lvm_lv array"
-   | RStat _ -> pr "stat"
-   | RStatVFS _ -> pr "statvfs"
-   | RHashtable _ -> pr "(string * string) list"
-  );
-  if is_external then (
-    pr " = ";
-    if List.length (snd style) + 1 > 5 then
-      pr "\"ocaml_guestfs_%s_byte\" " name;
-    pr "\"ocaml_guestfs_%s\"" name
-  );
-  pr "\n"
+  for (argc = 0; argv[argc] != NULL; ++argc)
+    ;
 
-(* Generate Perl xs code, a sort of crazy variation of C with macros. *)
-and generate_perl_xs () =
-  generate_header CStyle LGPLv2;
+  list = PyList_New (argc);
+  for (i = 0; i < argc; ++i)
+    PyList_SetItem (list, i, PyString_FromString (argv[i]));
 
-  pr "\
-#include \"EXTERN.h\"
-#include \"perl.h\"
-#include \"XSUB.h\"
+  return list;
+}
 
-#include <guestfs.h>
+static PyObject *
+put_table (char * const * const argv)
+{
+  PyObject *list, *item;
+  int argc, i;
 
-#ifndef PRId64
-#define PRId64 \"lld\"
-#endif
+  for (argc = 0; argv[argc] != NULL; ++argc)
+    ;
 
-static SV *
-my_newSVll(long long val) {
-#ifdef USE_64_BIT_ALL
-  return newSViv(val);
-#else
-  char buf[100];
-  int len;
-  len = snprintf(buf, 100, \"%%\" PRId64, val);
-  return newSVpv(buf, len);
-#endif
+  list = PyList_New (argc >> 1);
+  for (i = 0; i < argc; i += 2) {
+    item = PyTuple_New (2);
+    PyTuple_SetItem (item, 0, PyString_FromString (argv[i]));
+    PyTuple_SetItem (item, 1, PyString_FromString (argv[i+1]));
+    PyList_SetItem (list, i >> 1, item);
+  }
+
+  return list;
 }
 
-#ifndef PRIu64
-#define PRIu64 \"llu\"
-#endif
+static void
+free_strings (char **argv)
+{
+  int argc;
 
-static SV *
-my_newSVull(unsigned long long val) {
-#ifdef USE_64_BIT_ALL
-  return newSVuv(val);
-#else
-  char buf[100];
-  int len;
-  len = snprintf(buf, 100, \"%%\" PRIu64, val);
-  return newSVpv(buf, len);
-#endif
+  for (argc = 0; argv[argc] != NULL; ++argc)
+    free (argv[argc]);
+  free (argv);
 }
 
-/* http://www.perlmonks.org/?node_id=680842 */
-static char **
-XS_unpack_charPtrPtr (SV *arg) {
-  char **ret;
-  AV *av;
-  I32 i;
+static PyObject *
+py_guestfs_create (PyObject *self, PyObject *args)
+{
+  guestfs_h *g;
 
-  if (!arg || !SvOK (arg) || !SvROK (arg) || SvTYPE (SvRV (arg)) != SVt_PVAV) {
-    croak (\"array reference expected\");
+  g = guestfs_create ();
+  if (g == NULL) {
+    PyErr_SetString (PyExc_RuntimeError,
+                     \"guestfs.create: failed to allocate handle\");
+    return NULL;
   }
+  guestfs_set_error_handler (g, NULL, NULL);
+  return put_handle (g);
+}
 
-  av = (AV *)SvRV (arg);
-  ret = (char **)malloc (av_len (av) + 1 + 1);
+static PyObject *
+py_guestfs_close (PyObject *self, PyObject *args)
+{
+  PyObject *py_g;
+  guestfs_h *g;
+
+  if (!PyArg_ParseTuple (args, (char *) \"O:guestfs_close\", &py_g))
+    return NULL;
+  g = get_handle (py_g);
+
+  guestfs_close (g);
+
+  Py_INCREF (Py_None);
+  return Py_None;
+}
+
+";
+
+  (* LVM structures, turned into Python dictionaries. *)
+  List.iter (
+    fun (typ, cols) ->
+      pr "static PyObject *\n";
+      pr "put_lvm_%s (struct guestfs_lvm_%s *%s)\n" typ typ typ;
+      pr "{\n";
+      pr "  PyObject *dict;\n";
+      pr "\n";
+      pr "  dict = PyDict_New ();\n";
+      List.iter (
+       function
+       | name, `String ->
+           pr "  PyDict_SetItemString (dict, \"%s\",\n" name;
+           pr "                        PyString_FromString (%s->%s));\n"
+             typ name
+       | name, `UUID ->
+           pr "  PyDict_SetItemString (dict, \"%s\",\n" name;
+           pr "                        PyString_FromStringAndSize (%s->%s, 32));\n"
+             typ name
+       | name, `Bytes ->
+           pr "  PyDict_SetItemString (dict, \"%s\",\n" name;
+           pr "                        PyLong_FromUnsignedLongLong (%s->%s));\n"
+             typ name
+       | name, `Int ->
+           pr "  PyDict_SetItemString (dict, \"%s\",\n" name;
+           pr "                        PyLong_FromLongLong (%s->%s));\n"
+             typ name
+       | name, `OptPercent ->
+           pr "  if (%s->%s >= 0)\n" typ name;
+           pr "    PyDict_SetItemString (dict, \"%s\",\n" name;
+           pr "                          PyFloat_FromDouble ((double) %s->%s));\n"
+             typ name;
+           pr "  else {\n";
+           pr "    Py_INCREF (Py_None);\n";
+           pr "    PyDict_SetItemString (dict, \"%s\", Py_None);" name;
+           pr "  }\n"
+      ) cols;
+      pr "  return dict;\n";
+      pr "};\n";
+      pr "\n";
+
+      pr "static PyObject *\n";
+      pr "put_lvm_%s_list (struct guestfs_lvm_%s_list *%ss)\n" typ typ typ;
+      pr "{\n";
+      pr "  PyObject *list;\n";
+      pr "  int i;\n";
+      pr "\n";
+      pr "  list = PyList_New (%ss->len);\n" typ;
+      pr "  for (i = 0; i < %ss->len; ++i)\n" typ;
+      pr "    PyList_SetItem (list, i, put_lvm_%s (&%ss->val[i]));\n" typ typ;
+      pr "  return list;\n";
+      pr "};\n";
+      pr "\n"
+  ) ["pv", pv_cols; "vg", vg_cols; "lv", lv_cols];
+
+  (* Stat structures, turned into Python dictionaries. *)
+  List.iter (
+    fun (typ, cols) ->
+      pr "static PyObject *\n";
+      pr "put_%s (struct guestfs_%s *%s)\n" typ typ typ;
+      pr "{\n";
+      pr "  PyObject *dict;\n";
+      pr "\n";
+      pr "  dict = PyDict_New ();\n";
+      List.iter (
+       function
+       | name, `Int ->
+           pr "  PyDict_SetItemString (dict, \"%s\",\n" name;
+           pr "                        PyLong_FromLongLong (%s->%s));\n"
+             typ name
+      ) cols;
+      pr "  return dict;\n";
+      pr "};\n";
+      pr "\n";
+  ) ["stat", stat_cols; "statvfs", statvfs_cols];
 
-  for (i = 0; i <= av_len (av); i++) {
-    SV **elem = av_fetch (av, i, 0);
+  (* Python wrapper functions. *)
+  List.iter (
+    fun (name, style, _, _, _, _, _) ->
+      pr "static PyObject *\n";
+      pr "py_guestfs_%s (PyObject *self, PyObject *args)\n" name;
+      pr "{\n";
 
-    if (!elem || !*elem)
-      croak (\"missing element in list\");
+      pr "  PyObject *py_g;\n";
+      pr "  guestfs_h *g;\n";
+      pr "  PyObject *py_r;\n";
 
-    ret[i] = SvPV_nolen (*elem);
-  }
+      let error_code =
+       match fst style with
+       | RErr | RInt _ | RBool _ -> pr "  int r;\n"; "-1"
+       | RInt64 _ -> pr "  int64_t r;\n"; "-1"
+       | RConstString _ -> pr "  const char *r;\n"; "NULL"
+       | RString _ -> pr "  char *r;\n"; "NULL"
+       | RStringList _ | RHashtable _ -> pr "  char **r;\n"; "NULL"
+       | RIntBool _ -> pr "  struct guestfs_int_bool *r;\n"; "NULL"
+       | RPVList n -> pr "  struct guestfs_lvm_pv_list *r;\n"; "NULL"
+       | RVGList n -> pr "  struct guestfs_lvm_vg_list *r;\n"; "NULL"
+       | RLVList n -> pr "  struct guestfs_lvm_lv_list *r;\n"; "NULL"
+       | RStat n -> pr "  struct guestfs_stat *r;\n"; "NULL"
+       | RStatVFS n -> pr "  struct guestfs_statvfs *r;\n"; "NULL" in
 
-  ret[i] = NULL;
+      List.iter (
+       function
+       | String n | FileIn n | FileOut n -> pr "  const char *%s;\n" n
+       | OptString n -> pr "  const char *%s;\n" n
+       | StringList n ->
+           pr "  PyObject *py_%s;\n" n;
+           pr "  const char **%s;\n" n
+       | Bool n -> pr "  int %s;\n" n
+       | Int n -> pr "  int %s;\n" n
+      ) (snd style);
 
-  return ret;
-}
+      pr "\n";
 
-MODULE = Sys::Guestfs  PACKAGE = Sys::Guestfs
+      (* Convert the parameters. *)
+      pr "  if (!PyArg_ParseTuple (args, (char *) \"O";
+      List.iter (
+       function
+       | String _ | FileIn _ | FileOut _ -> pr "s"
+       | OptString _ -> pr "z"
+       | StringList _ -> pr "O"
+       | Bool _ -> pr "i" (* XXX Python has booleans? *)
+       | Int _ -> pr "i"
+      ) (snd style);
+      pr ":guestfs_%s\",\n" name;
+      pr "                         &py_g";
+      List.iter (
+       function
+       | String n | FileIn n | FileOut n -> pr ", &%s" n
+       | OptString n -> pr ", &%s" n
+       | StringList n -> pr ", &py_%s" n
+       | Bool n -> pr ", &%s" n
+       | Int n -> pr ", &%s" n
+      ) (snd style);
 
-guestfs_h *
-_create ()
-   CODE:
-      RETVAL = guestfs_create ();
-      if (!RETVAL)
-        croak (\"could not create guestfs handle\");
-      guestfs_set_error_handler (RETVAL, NULL, NULL);
- OUTPUT:
-      RETVAL
+      pr "))\n";
+      pr "    return NULL;\n";
 
-void
-DESTROY (g)
-      guestfs_h *g;
- PPCODE:
-      guestfs_close (g);
+      pr "  g = get_handle (py_g);\n";
+      List.iter (
+       function
+       | String _ | FileIn _ | FileOut _ | OptString _ | Bool _ | Int _ -> ()
+       | StringList n ->
+           pr "  %s = get_string_list (py_%s);\n" n n;
+           pr "  if (!%s) return NULL;\n" n
+      ) (snd style);
 
-";
+      pr "\n";
 
-  List.iter (
-    fun (name, style, _, _, _, _, _) ->
-      (match fst style with
-       | RErr -> pr "void\n"
-       | RInt _ -> pr "SV *\n"
-       | RInt64 _ -> pr "SV *\n"
-       | RBool _ -> pr "SV *\n"
-       | RConstString _ -> pr "SV *\n"
-       | RString _ -> pr "SV *\n"
-       | RStringList _
-       | RIntBool _
-       | RPVList _ | RVGList _ | RLVList _
-       | RStat _ | RStatVFS _
-       | RHashtable _ ->
-          pr "void\n" (* all lists returned implictly on the stack *)
-      );
-      (* Call and arguments. *)
-      pr "%s " name;
+      pr "  r = guestfs_%s " name;
       generate_call_args ~handle:"g" (snd style);
-      pr "\n";
-      pr "      guestfs_h *g;\n";
+      pr ";\n";
+
       List.iter (
        function
-       | String n | FileIn n | FileOut n -> pr "      char *%s;\n" n
-       | OptString n -> pr "      char *%s;\n" n
-       | StringList n -> pr "      char **%s;\n" n
-       | Bool n -> pr "      int %s;\n" n
-       | Int n -> pr "      int %s;\n" n
+       | String _ | FileIn _ | FileOut _ | OptString _ | Bool _ | Int _ -> ()
+       | StringList n ->
+           pr "  free (%s);\n" n
       ) (snd style);
 
-      let do_cleanups () =
-       List.iter (
-         function
-         | String _ | OptString _ | Bool _ | Int _
-         | FileIn _ | FileOut _ -> ()
-         | StringList n -> pr "      free (%s);\n" n
-       ) (snd style)
-      in
+      pr "  if (r == %s) {\n" error_code;
+      pr "    PyErr_SetString (PyExc_RuntimeError, guestfs_last_error (g));\n";
+      pr "    return NULL;\n";
+      pr "  }\n";
+      pr "\n";
 
-      (* Code. *)
       (match fst style with
        | RErr ->
-          pr "PREINIT:\n";
-          pr "      int r;\n";
-          pr " PPCODE:\n";
-          pr "      r = guestfs_%s " name;
-          generate_call_args ~handle:"g" (snd style);
-          pr ";\n";
-          do_cleanups ();
-          pr "      if (r == -1)\n";
-          pr "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
-       | RInt n
-       | RBool n ->
-          pr "PREINIT:\n";
-          pr "      int %s;\n" n;
-          pr "   CODE:\n";
-          pr "      %s = guestfs_%s " n name;
-          generate_call_args ~handle:"g" (snd style);
-          pr ";\n";
-          do_cleanups ();
-          pr "      if (%s == -1)\n" n;
-          pr "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
-          pr "      RETVAL = newSViv (%s);\n" n;
-          pr " OUTPUT:\n";
-          pr "      RETVAL\n"
-       | RInt64 n ->
-          pr "PREINIT:\n";
-          pr "      int64_t %s;\n" n;
-          pr "   CODE:\n";
-          pr "      %s = guestfs_%s " n name;
-          generate_call_args ~handle:"g" (snd style);
-          pr ";\n";
-          do_cleanups ();
-          pr "      if (%s == -1)\n" n;
-          pr "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
-          pr "      RETVAL = my_newSVll (%s);\n" n;
-          pr " OUTPUT:\n";
-          pr "      RETVAL\n"
-       | RConstString n ->
-          pr "PREINIT:\n";
-          pr "      const char *%s;\n" n;
-          pr "   CODE:\n";
-          pr "      %s = guestfs_%s " n name;
-          generate_call_args ~handle:"g" (snd style);
-          pr ";\n";
-          do_cleanups ();
-          pr "      if (%s == NULL)\n" n;
-          pr "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
-          pr "      RETVAL = newSVpv (%s, 0);\n" n;
-          pr " OUTPUT:\n";
-          pr "      RETVAL\n"
-       | RString n ->
-          pr "PREINIT:\n";
-          pr "      char *%s;\n" n;
-          pr "   CODE:\n";
-          pr "      %s = guestfs_%s " n name;
-          generate_call_args ~handle:"g" (snd style);
-          pr ";\n";
-          do_cleanups ();
-          pr "      if (%s == NULL)\n" n;
-          pr "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
-          pr "      RETVAL = newSVpv (%s, 0);\n" n;
-          pr "      free (%s);\n" n;
-          pr " OUTPUT:\n";
-          pr "      RETVAL\n"
-       | RStringList n | RHashtable n ->
-          pr "PREINIT:\n";
-          pr "      char **%s;\n" n;
-          pr "      int i, n;\n";
-          pr " PPCODE:\n";
-          pr "      %s = guestfs_%s " n name;
-          generate_call_args ~handle:"g" (snd style);
-          pr ";\n";
-          do_cleanups ();
-          pr "      if (%s == NULL)\n" n;
-          pr "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
-          pr "      for (n = 0; %s[n] != NULL; ++n) /**/;\n" n;
-          pr "      EXTEND (SP, n);\n";
-          pr "      for (i = 0; i < n; ++i) {\n";
-          pr "        PUSHs (sv_2mortal (newSVpv (%s[i], 0)));\n" n;
-          pr "        free (%s[i]);\n" n;
-          pr "      }\n";
-          pr "      free (%s);\n" n;
+          pr "  Py_INCREF (Py_None);\n";
+          pr "  py_r = Py_None;\n"
+       | RInt _
+       | RBool _ -> pr "  py_r = PyInt_FromLong ((long) r);\n"
+       | RInt64 _ -> pr "  py_r = PyLong_FromLongLong (r);\n"
+       | RConstString _ -> pr "  py_r = PyString_FromString (r);\n"
+       | RString _ ->
+          pr "  py_r = PyString_FromString (r);\n";
+          pr "  free (r);\n"
+       | RStringList _ ->
+          pr "  py_r = put_string_list (r);\n";
+          pr "  free_strings (r);\n"
        | RIntBool _ ->
-          pr "PREINIT:\n";
-          pr "      struct guestfs_int_bool *r;\n";
-          pr " PPCODE:\n";
-          pr "      r = guestfs_%s " name;
-          generate_call_args ~handle:"g" (snd style);
-          pr ";\n";
-          do_cleanups ();
-          pr "      if (r == NULL)\n";
-          pr "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
-          pr "      EXTEND (SP, 2);\n";
-          pr "      PUSHs (sv_2mortal (newSViv (r->i)));\n";
-          pr "      PUSHs (sv_2mortal (newSViv (r->b)));\n";
-          pr "      guestfs_free_int_bool (r);\n";
+          pr "  py_r = PyTuple_New (2);\n";
+          pr "  PyTuple_SetItem (py_r, 0, PyInt_FromLong ((long) r->i));\n";
+          pr "  PyTuple_SetItem (py_r, 1, PyInt_FromLong ((long) r->b));\n";
+          pr "  guestfs_free_int_bool (r);\n"
        | RPVList n ->
-          generate_perl_lvm_code "pv" pv_cols name style n do_cleanups
+          pr "  py_r = put_lvm_pv_list (r);\n";
+          pr "  guestfs_free_lvm_pv_list (r);\n"
        | RVGList n ->
-          generate_perl_lvm_code "vg" vg_cols name style n do_cleanups
+          pr "  py_r = put_lvm_vg_list (r);\n";
+          pr "  guestfs_free_lvm_vg_list (r);\n"
        | RLVList n ->
-          generate_perl_lvm_code "lv" lv_cols name style n do_cleanups
+          pr "  py_r = put_lvm_lv_list (r);\n";
+          pr "  guestfs_free_lvm_lv_list (r);\n"
        | RStat n ->
-          generate_perl_stat_code "stat" stat_cols name style n do_cleanups
+          pr "  py_r = put_stat (r);\n";
+          pr "  free (r);\n"
        | RStatVFS n ->
-          generate_perl_stat_code
-            "statvfs" statvfs_cols name style n do_cleanups
+          pr "  py_r = put_statvfs (r);\n";
+          pr "  free (r);\n"
+       | RHashtable n ->
+          pr "  py_r = put_table (r);\n";
+          pr "  free_strings (r);\n"
       );
 
+      pr "  return py_r;\n";
+      pr "}\n";
       pr "\n"
-  ) all_functions
-
-and generate_perl_lvm_code typ cols name style n do_cleanups =
-  pr "PREINIT:\n";
-  pr "      struct guestfs_lvm_%s_list *%s;\n" typ n;
-  pr "      int i;\n";
-  pr "      HV *hv;\n";
-  pr " PPCODE:\n";
-  pr "      %s = guestfs_%s " n name;
-  generate_call_args ~handle:"g" (snd style);
-  pr ";\n";
-  do_cleanups ();
-  pr "      if (%s == NULL)\n" n;
-  pr "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
-  pr "      EXTEND (SP, %s->len);\n" n;
-  pr "      for (i = 0; i < %s->len; ++i) {\n" n;
-  pr "        hv = newHV ();\n";
-  List.iter (
-    function
-    | name, `String ->
-       pr "        (void) hv_store (hv, \"%s\", %d, newSVpv (%s->val[i].%s, 0), 0);\n"
-         name (String.length name) n name
-    | name, `UUID ->
-       pr "        (void) hv_store (hv, \"%s\", %d, newSVpv (%s->val[i].%s, 32), 0);\n"
-         name (String.length name) n name
-    | name, `Bytes ->
-       pr "        (void) hv_store (hv, \"%s\", %d, my_newSVull (%s->val[i].%s), 0);\n"
-         name (String.length name) n name
-    | name, `Int ->
-       pr "        (void) hv_store (hv, \"%s\", %d, my_newSVll (%s->val[i].%s), 0);\n"
-         name (String.length name) n name
-    | name, `OptPercent ->
-       pr "        (void) hv_store (hv, \"%s\", %d, newSVnv (%s->val[i].%s), 0);\n"
-         name (String.length name) n name
-  ) cols;
-  pr "        PUSHs (sv_2mortal ((SV *) hv));\n";
-  pr "      }\n";
-  pr "      guestfs_free_lvm_%s_list (%s);\n" typ n
+  ) all_functions;
 
-and generate_perl_stat_code typ cols name style n do_cleanups =
-  pr "PREINIT:\n";
-  pr "      struct guestfs_%s *%s;\n" typ n;
-  pr " PPCODE:\n";
-  pr "      %s = guestfs_%s " n name;
-  generate_call_args ~handle:"g" (snd style);
-  pr ";\n";
-  do_cleanups ();
-  pr "      if (%s == NULL)\n" n;
-  pr "        croak (\"%s: %%s\", guestfs_last_error (g));\n" name;
-  pr "      EXTEND (SP, %d);\n" (List.length cols);
+  (* Table of functions. *)
+  pr "static PyMethodDef methods[] = {\n";
+  pr "  { (char *) \"create\", py_guestfs_create, METH_VARARGS, NULL },\n";
+  pr "  { (char *) \"close\", py_guestfs_close, METH_VARARGS, NULL },\n";
   List.iter (
-    function
-    | name, `Int ->
-       pr "      PUSHs (sv_2mortal (my_newSVll (%s->%s)));\n" n name
-  ) cols;
-  pr "      free (%s);\n" n
-
-(* Generate Sys/Guestfs.pm. *)
-and generate_perl_pm () =
-  generate_header HashStyle LGPLv2;
+    fun (name, _, _, _, _, _, _) ->
+      pr "  { (char *) \"%s\", py_guestfs_%s, METH_VARARGS, NULL },\n"
+       name name
+  ) all_functions;
+  pr "  { NULL, NULL, 0, NULL }\n";
+  pr "};\n";
+  pr "\n";
 
+  (* Init function. *)
   pr "\
-=pod
-
-=head1 NAME
+void
+initlibguestfsmod (void)
+{
+  static int initialized = 0;
 
-Sys::Guestfs - Perl bindings for libguestfs
+  if (initialized) return;
+  Py_InitModule ((char *) \"libguestfsmod\", methods);
+  initialized = 1;
+}
+"
 
-=head1 SYNOPSIS
+(* Generate Python module. *)
+and generate_python_py () =
+  generate_header HashStyle LGPLv2;
 
- use Sys::Guestfs;
- my $h = Sys::Guestfs->new ();
- $h->add_drive ('guest.img');
- $h->launch ();
- $h->wait_ready ();
- $h->mount ('/dev/sda1', '/');
- $h->touch ('/hello');
- $h->sync ();
+  pr "\
+u\"\"\"Python bindings for libguestfs
 
-=head1 DESCRIPTION
+import guestfs
+g = guestfs.GuestFS ()
+g.add_drive (\"guest.img\")
+g.launch ()
+g.wait_ready ()
+parts = g.list_partitions ()
 
-The C<Sys::Guestfs> module provides a Perl XS binding to the
-libguestfs API for examining and modifying virtual machine
-disk images.
+The guestfs module provides a Python binding to the libguestfs API
+for examining and modifying virtual machine disk images.
 
 Amongst the things this is good for: making batch configuration
 changes to guests, getting disk used/free statistics (see also:
@@ -4483,338 +6415,219 @@ Libguestfs provides ways to enumerate guest storage (eg. partitions,
 LVs, what filesystem is in each LV, etc.).  It can also run commands
 in the context of the guest.  Also you can access filesystems over FTP.
 
-=head1 ERRORS
-
-All errors turn into calls to C<croak> (see L<Carp(3)>).
-
-=head1 METHODS
-
-=over 4
-
-=cut
-
-package Sys::Guestfs;
-
-use strict;
-use warnings;
-
-require XSLoader;
-XSLoader::load ('Sys::Guestfs');
-
-=item $h = Sys::Guestfs->new ();
-
-Create a new guestfs handle.
-
-=cut
-
-sub new {
-  my $proto = shift;
-  my $class = ref ($proto) || $proto;
-
-  my $self = Sys::Guestfs::_create ();
-  bless $self, $class;
-  return $self;
-}
-
-";
-
-  (* Actions.  We only need to print documentation for these as
-   * they are pulled in from the XS code automatically.
-   *)
-  List.iter (
-    fun (name, style, _, flags, _, _, longdesc) ->
-      let longdesc = replace_str longdesc "C<guestfs_" "C<$h-E<gt>" in
-      pr "=item ";
-      generate_perl_prototype name style;
-      pr "\n\n";
-      pr "%s\n\n" longdesc;
-      if List.mem ProtocolLimitWarning flags then
-       pr "%s\n\n" protocol_limit_warning;
-      if List.mem DangerWillRobinson flags then
-       pr "%s\n\n" danger_will_robinson
-  ) all_functions_sorted;
-
-  (* End of file. *)
-  pr "\
-=cut
-
-1;
-
-=back
-
-=head1 COPYRIGHT
-
-Copyright (C) 2009 Red Hat Inc.
-
-=head1 LICENSE
-
-Please see the file COPYING.LIB for the full license.
-
-=head1 SEE ALSO
-
-L<guestfs(3)>, L<guestfish(1)>.
-
-=cut
-"
-
-and generate_perl_prototype name style =
-  (match fst style with
-   | RErr -> ()
-   | RBool n
-   | RInt n
-   | RInt64 n
-   | RConstString n
-   | RString n -> pr "$%s = " n
-   | RIntBool (n, m) -> pr "($%s, $%s) = " n m
-   | RStringList n
-   | RPVList n
-   | RVGList n
-   | RLVList n -> pr "@%s = " n
-   | RStat n
-   | RStatVFS n
-   | RHashtable n -> pr "%%%s = " n
-  );
-  pr "$h->%s (" name;
-  let comma = ref false in
-  List.iter (
-    fun arg ->
-      if !comma then pr ", ";
-      comma := true;
-      match arg with
-      | String n | OptString n | Bool n | Int n | FileIn n | FileOut n ->
-         pr "$%s" n
-      | StringList n ->
-         pr "\\@%s" n
-  ) (snd style);
-  pr ");"
-
-(* Generate Python C module. *)
-and generate_python_c () =
-  generate_header CStyle LGPLv2;
+Errors which happen while using the API are turned into Python
+RuntimeError exceptions.
 
-  pr "\
-#include <stdio.h>
-#include <stdlib.h>
-#include <assert.h>
+To create a guestfs handle you usually have to perform the following
+sequence of calls:
 
-#include <Python.h>
+# Create the handle, call add_drive at least once, and possibly
+# several times if the guest has multiple block devices:
+g = guestfs.GuestFS ()
+g.add_drive (\"guest.img\")
 
-#include \"guestfs.h\"
+# Launch the qemu subprocess and wait for it to become ready:
+g.launch ()
+g.wait_ready ()
 
-typedef struct {
-  PyObject_HEAD
-  guestfs_h *g;
-} Pyguestfs_Object;
+# Now you can issue commands, for example:
+logvols = g.lvs ()
 
-static guestfs_h *
-get_handle (PyObject *obj)
-{
-  assert (obj);
-  assert (obj != Py_None);
-  return ((Pyguestfs_Object *) obj)->g;
-}
+\"\"\"
 
-static PyObject *
-put_handle (guestfs_h *g)
-{
-  assert (g);
-  return
-    PyCObject_FromVoidPtrAndDesc ((void *) g, (char *) \"guestfs_h\", NULL);
-}
+import libguestfsmod
 
-/* This list should be freed (but not the strings) after use. */
-static const char **
-get_string_list (PyObject *obj)
-{
-  int i, len;
-  const char **r;
+class GuestFS:
+    \"\"\"Instances of this class are libguestfs API handles.\"\"\"
 
-  assert (obj);
+    def __init__ (self):
+        \"\"\"Create a new libguestfs handle.\"\"\"
+        self._o = libguestfsmod.create ()
 
-  if (!PyList_Check (obj)) {
-    PyErr_SetString (PyExc_RuntimeError, \"expecting a list parameter\");
-    return NULL;
-  }
+    def __del__ (self):
+        libguestfsmod.close (self._o)
 
-  len = PyList_Size (obj);
-  r = malloc (sizeof (char *) * (len+1));
-  if (r == NULL) {
-    PyErr_SetString (PyExc_RuntimeError, \"get_string_list: out of memory\");
-    return NULL;
-  }
+";
 
-  for (i = 0; i < len; ++i)
-    r[i] = PyString_AsString (PyList_GetItem (obj, i));
-  r[len] = NULL;
+  List.iter (
+    fun (name, style, _, flags, _, _, longdesc) ->
+      pr "    def %s " name;
+      generate_call_args ~handle:"self" (snd style);
+      pr ":\n";
 
-  return r;
-}
+      if not (List.mem NotInDocs flags) then (
+       let doc = replace_str longdesc "C<guestfs_" "C<g." in
+       let doc =
+          match fst style with
+         | RErr | RInt _ | RInt64 _ | RBool _ | RConstString _
+         | RString _ -> doc
+         | RStringList _ ->
+             doc ^ "\n\nThis function returns a list of strings."
+         | RIntBool _ ->
+             doc ^ "\n\nThis function returns a tuple (int, bool).\n"
+         | RPVList _ ->
+             doc ^ "\n\nThis function returns a list of PVs.  Each PV is represented as a dictionary."
+         | RVGList _ ->
+             doc ^ "\n\nThis function returns a list of VGs.  Each VG is represented as a dictionary."
+         | RLVList _ ->
+             doc ^ "\n\nThis function returns a list of LVs.  Each LV is represented as a dictionary."
+         | RStat _ ->
+             doc ^ "\n\nThis function returns a dictionary, with keys matching the various fields in the stat structure."
+         | RStatVFS _ ->
+             doc ^ "\n\nThis function returns a dictionary, with keys matching the various fields in the statvfs structure."
+         | RHashtable _ ->
+             doc ^ "\n\nThis function returns a dictionary." in
+       let doc =
+         if List.mem ProtocolLimitWarning flags then
+           doc ^ "\n\n" ^ protocol_limit_warning
+         else doc in
+       let doc =
+         if List.mem DangerWillRobinson flags then
+           doc ^ "\n\n" ^ danger_will_robinson
+         else doc in
+       let doc = pod2text ~width:60 name doc in
+       let doc = List.map (fun line -> replace_str line "\\" "\\\\") doc in
+       let doc = String.concat "\n        " doc in
+       pr "        u\"\"\"%s\"\"\"\n" doc;
+      );
+      pr "        return libguestfsmod.%s " name;
+      generate_call_args ~handle:"self._o" (snd style);
+      pr "\n";
+      pr "\n";
+  ) all_functions
 
-static PyObject *
-put_string_list (char * const * const argv)
-{
-  PyObject *list;
-  int argc, i;
+(* Useful if you need the longdesc POD text as plain text.  Returns a
+ * list of lines.
+ *
+ * This is the slowest thing about autogeneration.
+ *)
+and pod2text ~width name longdesc =
+  let filename, chan = Filename.open_temp_file "gen" ".tmp" in
+  fprintf chan "=head1 %s\n\n%s\n" name longdesc;
+  close_out chan;
+  let cmd = sprintf "pod2text -w %d %s" width (Filename.quote filename) in
+  let chan = Unix.open_process_in cmd in
+  let lines = ref [] in
+  let rec loop i =
+    let line = input_line chan in
+    if i = 1 then              (* discard the first line of output *)
+      loop (i+1)
+    else (
+      let line = triml line in
+      lines := line :: !lines;
+      loop (i+1)
+    ) in
+  let lines = try loop 1 with End_of_file -> List.rev !lines in
+  Unix.unlink filename;
+  match Unix.close_process_in chan with
+  | Unix.WEXITED 0 -> lines
+  | Unix.WEXITED i ->
+      failwithf "pod2text: process exited with non-zero status (%d)" i
+  | Unix.WSIGNALED i | Unix.WSTOPPED i ->
+      failwithf "pod2text: process signalled or stopped by signal %d" i
 
-  for (argc = 0; argv[argc] != NULL; ++argc)
-    ;
+(* Generate ruby bindings. *)
+and generate_ruby_c () =
+  generate_header CStyle LGPLv2;
 
-  list = PyList_New (argc);
-  for (i = 0; i < argc; ++i)
-    PyList_SetItem (list, i, PyString_FromString (argv[i]));
+  pr "\
+#include <stdio.h>
+#include <stdlib.h>
 
-  return list;
-}
+#include <ruby.h>
 
-static PyObject *
-put_table (char * const * const argv)
-{
-  PyObject *list, *item;
-  int argc, i;
+#include \"guestfs.h\"
 
-  for (argc = 0; argv[argc] != NULL; ++argc)
-    ;
+#include \"extconf.h\"
 
-  list = PyList_New (argc >> 1);
-  for (i = 0; i < argc; i += 2) {
-    item = PyTuple_New (2);
-    PyTuple_SetItem (item, 0, PyString_FromString (argv[i]));
-    PyTuple_SetItem (item, 1, PyString_FromString (argv[i+1]));
-    PyList_SetItem (list, i >> 1, item);
-  }
+/* For Ruby < 1.9 */
+#ifndef RARRAY_LEN
+#define RARRAY_LEN(r) (RARRAY((r))->len)
+#endif
 
-  return list;
-}
+static VALUE m_guestfs;                        /* guestfs module */
+static VALUE c_guestfs;                        /* guestfs_h handle */
+static VALUE e_Error;                  /* used for all errors */
 
-static void
-free_strings (char **argv)
+static void ruby_guestfs_free (void *p)
 {
-  int argc;
-
-  for (argc = 0; argv[argc] != NULL; ++argc)
-    free (argv[argc]);
-  free (argv);
+  if (!p) return;
+  guestfs_close ((guestfs_h *) p);
 }
 
-static PyObject *
-py_guestfs_create (PyObject *self, PyObject *args)
+static VALUE ruby_guestfs_create (VALUE m)
 {
   guestfs_h *g;
 
   g = guestfs_create ();
-  if (g == NULL) {
-    PyErr_SetString (PyExc_RuntimeError,
-                     \"guestfs.create: failed to allocate handle\");
-    return NULL;
-  }
+  if (!g)
+    rb_raise (e_Error, \"failed to create guestfs handle\");
+
+  /* Don't print error messages to stderr by default. */
   guestfs_set_error_handler (g, NULL, NULL);
-  return put_handle (g);
+
+  /* Wrap it, and make sure the close function is called when the
+   * handle goes away.
+   */
+  return Data_Wrap_Struct (c_guestfs, NULL, ruby_guestfs_free, g);
 }
 
-static PyObject *
-py_guestfs_close (PyObject *self, PyObject *args)
+static VALUE ruby_guestfs_close (VALUE gv)
 {
-  PyObject *py_g;
   guestfs_h *g;
+  Data_Get_Struct (gv, guestfs_h, g);
 
-  if (!PyArg_ParseTuple (args, (char *) \"O:guestfs_close\", &py_g))
-    return NULL;
-  g = get_handle (py_g);
-
-  guestfs_close (g);
+  ruby_guestfs_free (g);
+  DATA_PTR (gv) = NULL;
 
-  Py_INCREF (Py_None);
-  return Py_None;
+  return Qnil;
 }
 
 ";
 
-  (* LVM structures, turned into Python dictionaries. *)
-  List.iter (
-    fun (typ, cols) ->
-      pr "static PyObject *\n";
-      pr "put_lvm_%s (struct guestfs_lvm_%s *%s)\n" typ typ typ;
-      pr "{\n";
-      pr "  PyObject *dict;\n";
-      pr "\n";
-      pr "  dict = PyDict_New ();\n";
-      List.iter (
-       function
-       | name, `String ->
-           pr "  PyDict_SetItemString (dict, \"%s\",\n" name;
-           pr "                        PyString_FromString (%s->%s));\n"
-             typ name
-       | name, `UUID ->
-           pr "  PyDict_SetItemString (dict, \"%s\",\n" name;
-           pr "                        PyString_FromStringAndSize (%s->%s, 32));\n"
-             typ name
-       | name, `Bytes ->
-           pr "  PyDict_SetItemString (dict, \"%s\",\n" name;
-           pr "                        PyLong_FromUnsignedLongLong (%s->%s));\n"
-             typ name
-       | name, `Int ->
-           pr "  PyDict_SetItemString (dict, \"%s\",\n" name;
-           pr "                        PyLong_FromLongLong (%s->%s));\n"
-             typ name
-       | name, `OptPercent ->
-           pr "  if (%s->%s >= 0)\n" typ name;
-           pr "    PyDict_SetItemString (dict, \"%s\",\n" name;
-           pr "                          PyFloat_FromDouble ((double) %s->%s));\n"
-             typ name;
-           pr "  else {\n";
-           pr "    Py_INCREF (Py_None);\n";
-           pr "    PyDict_SetItemString (dict, \"%s\", Py_None);" name;
-           pr "  }\n"
-      ) cols;
-      pr "  return dict;\n";
-      pr "};\n";
-      pr "\n";
-
-      pr "static PyObject *\n";
-      pr "put_lvm_%s_list (struct guestfs_lvm_%s_list *%ss)\n" typ typ typ;
-      pr "{\n";
-      pr "  PyObject *list;\n";
-      pr "  int i;\n";
-      pr "\n";
-      pr "  list = PyList_New (%ss->len);\n" typ;
-      pr "  for (i = 0; i < %ss->len; ++i)\n" typ;
-      pr "    PyList_SetItem (list, i, put_lvm_%s (&%ss->val[i]));\n" typ typ;
-      pr "  return list;\n";
-      pr "};\n";
-      pr "\n"
-  ) ["pv", pv_cols; "vg", vg_cols; "lv", lv_cols];
-
-  (* Stat structures, turned into Python dictionaries. *)
-  List.iter (
-    fun (typ, cols) ->
-      pr "static PyObject *\n";
-      pr "put_%s (struct guestfs_%s *%s)\n" typ typ typ;
-      pr "{\n";
-      pr "  PyObject *dict;\n";
-      pr "\n";
-      pr "  dict = PyDict_New ();\n";
-      List.iter (
-       function
-       | name, `Int ->
-           pr "  PyDict_SetItemString (dict, \"%s\",\n" name;
-           pr "                        PyLong_FromLongLong (%s->%s));\n"
-             typ name
-      ) cols;
-      pr "  return dict;\n";
-      pr "};\n";
-      pr "\n";
-  ) ["stat", stat_cols; "statvfs", statvfs_cols];
-
-  (* Python wrapper functions. *)
   List.iter (
     fun (name, style, _, _, _, _, _) ->
-      pr "static PyObject *\n";
-      pr "py_guestfs_%s (PyObject *self, PyObject *args)\n" name;
+      pr "static VALUE ruby_guestfs_%s (VALUE gv" name;
+      List.iter (fun arg -> pr ", VALUE %sv" (name_of_argt arg)) (snd style);
+      pr ")\n";
       pr "{\n";
+      pr "  guestfs_h *g;\n";
+      pr "  Data_Get_Struct (gv, guestfs_h, g);\n";
+      pr "  if (!g)\n";
+      pr "    rb_raise (rb_eArgError, \"%%s: used handle after closing it\", \"%s\");\n"
+       name;
+      pr "\n";
 
-      pr "  PyObject *py_g;\n";
-      pr "  guestfs_h *g;\n";
-      pr "  PyObject *py_r;\n";
+      List.iter (
+       function
+       | String n | FileIn n | FileOut n ->
+           pr "  Check_Type (%sv, T_STRING);\n" n;
+           pr "  const char *%s = StringValueCStr (%sv);\n" n n;
+           pr "  if (!%s)\n" n;
+           pr "    rb_raise (rb_eTypeError, \"expected string for parameter %%s of %%s\",\n";
+           pr "              \"%s\", \"%s\");\n" n name
+       | OptString n ->
+           pr "  const char *%s = !NIL_P (%sv) ? StringValueCStr (%sv) : NULL;\n" n n n
+       | StringList n ->
+           pr "  char **%s;\n" n;
+           pr "  Check_Type (%sv, T_ARRAY);\n" n;
+           pr "  {\n";
+           pr "    int i, len;\n";
+           pr "    len = RARRAY_LEN (%sv);\n" n;
+           pr "    %s = guestfs_safe_malloc (g, sizeof (char *) * (len+1));\n"
+             n;
+           pr "    for (i = 0; i < len; ++i) {\n";
+           pr "      VALUE v = rb_ary_entry (%sv, i);\n" n;
+           pr "      %s[i] = StringValueCStr (v);\n" n;
+           pr "    }\n";
+           pr "    %s[len] = NULL;\n" n;
+           pr "  }\n";
+       | Bool n ->
+           pr "  int %s = RTEST (%sv);\n" n n
+       | Int n ->
+           pr "  int %s = NUM2INT (%sv);\n" n n
+      ) (snd style);
+      pr "\n";
 
       let error_code =
        match fst style with
@@ -4829,53 +6642,6 @@ py_guestfs_close (PyObject *self, PyObject *args)
        | RLVList n -> pr "  struct guestfs_lvm_lv_list *r;\n"; "NULL"
        | RStat n -> pr "  struct guestfs_stat *r;\n"; "NULL"
        | RStatVFS n -> pr "  struct guestfs_statvfs *r;\n"; "NULL" in
-
-      List.iter (
-       function
-       | String n | FileIn n | FileOut n -> pr "  const char *%s;\n" n
-       | OptString n -> pr "  const char *%s;\n" n
-       | StringList n ->
-           pr "  PyObject *py_%s;\n" n;
-           pr "  const char **%s;\n" n
-       | Bool n -> pr "  int %s;\n" n
-       | Int n -> pr "  int %s;\n" n
-      ) (snd style);
-
-      pr "\n";
-
-      (* Convert the parameters. *)
-      pr "  if (!PyArg_ParseTuple (args, (char *) \"O";
-      List.iter (
-       function
-       | String _ | FileIn _ | FileOut _ -> pr "s"
-       | OptString _ -> pr "z"
-       | StringList _ -> pr "O"
-       | Bool _ -> pr "i" (* XXX Python has booleans? *)
-       | Int _ -> pr "i"
-      ) (snd style);
-      pr ":guestfs_%s\",\n" name;
-      pr "                         &py_g";
-      List.iter (
-       function
-       | String n | FileIn n | FileOut n -> pr ", &%s" n
-       | OptString n -> pr ", &%s" n
-       | StringList n -> pr ", &py_%s" n
-       | Bool n -> pr ", &%s" n
-       | Int n -> pr ", &%s" n
-      ) (snd style);
-
-      pr "))\n";
-      pr "    return NULL;\n";
-
-      pr "  g = get_handle (py_g);\n";
-      List.iter (
-       function
-       | String _ | FileIn _ | FileOut _ | OptString _ | Bool _ | Int _ -> ()
-       | StringList n ->
-           pr "  %s = get_string_list (py_%s);\n" n n;
-           pr "  if (!%s) return NULL;\n" n
-      ) (snd style);
-
       pr "\n";
 
       pr "  r = guestfs_%s " name;
@@ -4889,457 +6655,1284 @@ py_guestfs_close (PyObject *self, PyObject *args)
            pr "  free (%s);\n" n
       ) (snd style);
 
-      pr "  if (r == %s) {\n" error_code;
-      pr "    PyErr_SetString (PyExc_RuntimeError, guestfs_last_error (g));\n";
-      pr "    return NULL;\n";
-      pr "  }\n";
+      pr "  if (r == %s)\n" error_code;
+      pr "    rb_raise (e_Error, \"%%s\", guestfs_last_error (g));\n";
       pr "\n";
 
       (match fst style with
        | RErr ->
-          pr "  Py_INCREF (Py_None);\n";
-          pr "  py_r = Py_None;\n"
-       | RInt _
-       | RBool _ -> pr "  py_r = PyInt_FromLong ((long) r);\n"
-       | RInt64 _ -> pr "  py_r = PyLong_FromLongLong (r);\n"
-       | RConstString _ -> pr "  py_r = PyString_FromString (r);\n"
+          pr "  return Qnil;\n"
+       | RInt _ | RBool _ ->
+          pr "  return INT2NUM (r);\n"
+       | RInt64 _ ->
+          pr "  return ULL2NUM (r);\n"
+       | RConstString _ ->
+          pr "  return rb_str_new2 (r);\n";
        | RString _ ->
-          pr "  py_r = PyString_FromString (r);\n";
-          pr "  free (r);\n"
+          pr "  VALUE rv = rb_str_new2 (r);\n";
+          pr "  free (r);\n";
+          pr "  return rv;\n";
        | RStringList _ ->
-          pr "  py_r = put_string_list (r);\n";
-          pr "  free_strings (r);\n"
+          pr "  int i, len = 0;\n";
+          pr "  for (i = 0; r[i] != NULL; ++i) len++;\n";
+          pr "  VALUE rv = rb_ary_new2 (len);\n";
+          pr "  for (i = 0; r[i] != NULL; ++i) {\n";
+          pr "    rb_ary_push (rv, rb_str_new2 (r[i]));\n";
+          pr "    free (r[i]);\n";
+          pr "  }\n";
+          pr "  free (r);\n";
+          pr "  return rv;\n"
        | RIntBool _ ->
-          pr "  py_r = PyTuple_New (2);\n";
-          pr "  PyTuple_SetItem (py_r, 0, PyInt_FromLong ((long) r->i));\n";
-          pr "  PyTuple_SetItem (py_r, 1, PyInt_FromLong ((long) r->b));\n";
-          pr "  guestfs_free_int_bool (r);\n"
+          pr "  VALUE rv = rb_ary_new2 (2);\n";
+          pr "  rb_ary_push (rv, INT2NUM (r->i));\n";
+          pr "  rb_ary_push (rv, INT2NUM (r->b));\n";
+          pr "  guestfs_free_int_bool (r);\n";
+          pr "  return rv;\n"
        | RPVList n ->
-          pr "  py_r = put_lvm_pv_list (r);\n";
-          pr "  guestfs_free_lvm_pv_list (r);\n"
+          generate_ruby_lvm_code "pv" pv_cols
        | RVGList n ->
-          pr "  py_r = put_lvm_vg_list (r);\n";
-          pr "  guestfs_free_lvm_vg_list (r);\n"
+          generate_ruby_lvm_code "vg" vg_cols
        | RLVList n ->
-          pr "  py_r = put_lvm_lv_list (r);\n";
-          pr "  guestfs_free_lvm_lv_list (r);\n"
+          generate_ruby_lvm_code "lv" lv_cols
        | RStat n ->
-          pr "  py_r = put_stat (r);\n";
-          pr "  free (r);\n"
+          pr "  VALUE rv = rb_hash_new ();\n";
+          List.iter (
+            function
+            | name, `Int ->
+                pr "  rb_hash_aset (rv, rb_str_new2 (\"%s\"), ULL2NUM (r->%s));\n" name name
+          ) stat_cols;
+          pr "  free (r);\n";
+          pr "  return rv;\n"
        | RStatVFS n ->
-          pr "  py_r = put_statvfs (r);\n";
-          pr "  free (r);\n"
-       | RHashtable n ->
-          pr "  py_r = put_table (r);\n";
-          pr "  free_strings (r);\n"
+          pr "  VALUE rv = rb_hash_new ();\n";
+          List.iter (
+            function
+            | name, `Int ->
+                pr "  rb_hash_aset (rv, rb_str_new2 (\"%s\"), ULL2NUM (r->%s));\n" name name
+          ) statvfs_cols;
+          pr "  free (r);\n";
+          pr "  return rv;\n"
+       | RHashtable _ ->
+          pr "  VALUE rv = rb_hash_new ();\n";
+          pr "  int i;\n";
+          pr "  for (i = 0; r[i] != NULL; i+=2) {\n";
+          pr "    rb_hash_aset (rv, rb_str_new2 (r[i]), rb_str_new2 (r[i+1]));\n";
+          pr "    free (r[i]);\n";
+          pr "    free (r[i+1]);\n";
+          pr "  }\n";
+          pr "  free (r);\n";
+          pr "  return rv;\n"
       );
 
-      pr "  return py_r;\n";
       pr "}\n";
       pr "\n"
   ) all_functions;
 
-  (* Table of functions. *)
-  pr "static PyMethodDef methods[] = {\n";
-  pr "  { (char *) \"create\", py_guestfs_create, METH_VARARGS, NULL },\n";
-  pr "  { (char *) \"close\", py_guestfs_close, METH_VARARGS, NULL },\n";
+  pr "\
+/* Initialize the module. */
+void Init__guestfs ()
+{
+  m_guestfs = rb_define_module (\"Guestfs\");
+  c_guestfs = rb_define_class_under (m_guestfs, \"Guestfs\", rb_cObject);
+  e_Error = rb_define_class_under (m_guestfs, \"Error\", rb_eStandardError);
+
+  rb_define_module_function (m_guestfs, \"create\", ruby_guestfs_create, 0);
+  rb_define_method (c_guestfs, \"close\", ruby_guestfs_close, 0);
+
+";
+  (* Define the rest of the methods. *)
   List.iter (
-    fun (name, _, _, _, _, _, _) ->
-      pr "  { (char *) \"%s\", py_guestfs_%s, METH_VARARGS, NULL },\n"
-       name name
+    fun (name, style, _, _, _, _, _) ->
+      pr "  rb_define_method (c_guestfs, \"%s\",\n" name;
+      pr "        ruby_guestfs_%s, %d);\n" name (List.length (snd style))
   ) all_functions;
-  pr "  { NULL, NULL, 0, NULL }\n";
-  pr "};\n";
-  pr "\n";
 
-  (* Init function. *)
-  pr "\
-void
-initlibguestfsmod (void)
-{
-  static int initialized = 0;
+  pr "}\n"
 
-  if (initialized) return;
-  Py_InitModule ((char *) \"libguestfsmod\", methods);
-  initialized = 1;
-}
-"
+(* Ruby code to return an LVM struct list. *)
+and generate_ruby_lvm_code typ cols =
+  pr "  VALUE rv = rb_ary_new2 (r->len);\n";
+  pr "  int i;\n";
+  pr "  for (i = 0; i < r->len; ++i) {\n";
+  pr "    VALUE hv = rb_hash_new ();\n";
+  List.iter (
+    function
+    | name, `String ->
+       pr "    rb_hash_aset (rv, rb_str_new2 (\"%s\"), rb_str_new2 (r->val[i].%s));\n" name name
+    | name, `UUID ->
+       pr "    rb_hash_aset (rv, rb_str_new2 (\"%s\"), rb_str_new (r->val[i].%s, 32));\n" name name
+    | name, `Bytes
+    | name, `Int ->
+       pr "    rb_hash_aset (rv, rb_str_new2 (\"%s\"), ULL2NUM (r->val[i].%s));\n" name name
+    | name, `OptPercent ->
+       pr "    rb_hash_aset (rv, rb_str_new2 (\"%s\"), rb_dbl2big (r->val[i].%s));\n" name name
+  ) cols;
+  pr "    rb_ary_push (rv, hv);\n";
+  pr "  }\n";
+  pr "  guestfs_free_lvm_%s_list (r);\n" typ;
+  pr "  return rv;\n"
 
-(* Generate Python module. *)
-and generate_python_py () =
-  generate_header HashStyle LGPLv2;
+(* Generate Java bindings GuestFS.java file. *)
+and generate_java_java () =
+  generate_header CStyle LGPLv2;
 
   pr "\
-u\"\"\"Python bindings for libguestfs
+package com.redhat.et.libguestfs;
+
+import java.util.HashMap;
+import com.redhat.et.libguestfs.LibGuestFSException;
+import com.redhat.et.libguestfs.PV;
+import com.redhat.et.libguestfs.VG;
+import com.redhat.et.libguestfs.LV;
+import com.redhat.et.libguestfs.Stat;
+import com.redhat.et.libguestfs.StatVFS;
+import com.redhat.et.libguestfs.IntBool;
+
+/**
+ * The GuestFS object is a libguestfs handle.
+ *
+ * @author rjones
+ */
+public class GuestFS {
+  // Load the native code.
+  static {
+    System.loadLibrary (\"guestfs_jni\");
+  }
 
-import guestfs
-g = guestfs.GuestFS ()
-g.add_drive (\"guest.img\")
-g.launch ()
-g.wait_ready ()
-parts = g.list_partitions ()
+  /**
+   * The native guestfs_h pointer.
+   */
+  long g;
 
-The guestfs module provides a Python binding to the libguestfs API
-for examining and modifying virtual machine disk images.
+  /**
+   * Create a libguestfs handle.
+   *
+   * @throws LibGuestFSException
+   */
+  public GuestFS () throws LibGuestFSException
+  {
+    g = _create ();
+  }
+  private native long _create () throws LibGuestFSException;
 
-Amongst the things this is good for: making batch configuration
-changes to guests, getting disk used/free statistics (see also:
-virt-df), migrating between virtualization systems (see also:
-virt-p2v), performing partial backups, performing partial guest
-clones, cloning guests and changing registry/UUID/hostname info, and
-much else besides.
+  /**
+   * Close a libguestfs handle.
+   *
+   * You can also leave handles to be collected by the garbage
+   * collector, but this method ensures that the resources used
+   * by the handle are freed up immediately.  If you call any
+   * other methods after closing the handle, you will get an
+   * exception.
+   *
+   * @throws LibGuestFSException
+   */
+  public void close () throws LibGuestFSException
+  {
+    if (g != 0)
+      _close (g);
+    g = 0;
+  }
+  private native void _close (long g) throws LibGuestFSException;
 
-Libguestfs uses Linux kernel and qemu code, and can access any type of
-guest filesystem that Linux and qemu can, including but not limited
-to: ext2/3/4, btrfs, FAT and NTFS, LVM, many different disk partition
-schemes, qcow, qcow2, vmdk.
+  public void finalize () throws LibGuestFSException
+  {
+    close ();
+  }
 
-Libguestfs provides ways to enumerate guest storage (eg. partitions,
-LVs, what filesystem is in each LV, etc.).  It can also run commands
-in the context of the guest.  Also you can access filesystems over FTP.
+";
 
-Errors which happen while using the API are turned into Python
-RuntimeError exceptions.
+  List.iter (
+    fun (name, style, _, flags, _, shortdesc, longdesc) ->
+      if not (List.mem NotInDocs flags); then (
+       let doc = replace_str longdesc "C<guestfs_" "C<g." in
+       let doc =
+         if List.mem ProtocolLimitWarning flags then
+           doc ^ "\n\n" ^ protocol_limit_warning
+         else doc in
+       let doc =
+         if List.mem DangerWillRobinson flags then
+           doc ^ "\n\n" ^ danger_will_robinson
+         else doc in
+       let doc = pod2text ~width:60 name doc in
+       let doc = List.map (            (* RHBZ#501883 *)
+         function
+         | "" -> "<p>"
+         | nonempty -> nonempty
+       ) doc in
+       let doc = String.concat "\n   * " doc in
+
+       pr "  /**\n";
+       pr "   * %s\n" shortdesc;
+       pr "   * <p>\n";
+       pr "   * %s\n" doc;
+       pr "   * @throws LibGuestFSException\n";
+       pr "   */\n";
+       pr "  ";
+      );
+      generate_java_prototype ~public:true ~semicolon:false name style;
+      pr "\n";
+      pr "  {\n";
+      pr "    if (g == 0)\n";
+      pr "      throw new LibGuestFSException (\"%s: handle is closed\");\n"
+       name;
+      pr "    ";
+      if fst style <> RErr then pr "return ";
+      pr "_%s " name;
+      generate_call_args ~handle:"g" (snd style);
+      pr ";\n";
+      pr "  }\n";
+      pr "  ";
+      generate_java_prototype ~privat:true ~native:true name style;
+      pr "\n";
+      pr "\n";
+  ) all_functions;
 
-To create a guestfs handle you usually have to perform the following
-sequence of calls:
+  pr "}\n"
 
-# Create the handle, call add_drive at least once, and possibly
-# several times if the guest has multiple block devices:
-g = guestfs.GuestFS ()
-g.add_drive (\"guest.img\")
+and generate_java_prototype ?(public=false) ?(privat=false) ?(native=false)
+    ?(semicolon=true) name style =
+  if privat then pr "private ";
+  if public then pr "public ";
+  if native then pr "native ";
 
-# Launch the qemu subprocess and wait for it to become ready:
-g.launch ()
-g.wait_ready ()
+  (* return type *)
+  (match fst style with
+   | RErr -> pr "void ";
+   | RInt _ -> pr "int ";
+   | RInt64 _ -> pr "long ";
+   | RBool _ -> pr "boolean ";
+   | RConstString _ | RString _ -> pr "String ";
+   | RStringList _ -> pr "String[] ";
+   | RIntBool _ -> pr "IntBool ";
+   | RPVList _ -> pr "PV[] ";
+   | RVGList _ -> pr "VG[] ";
+   | RLVList _ -> pr "LV[] ";
+   | RStat _ -> pr "Stat ";
+   | RStatVFS _ -> pr "StatVFS ";
+   | RHashtable _ -> pr "HashMap<String,String> ";
+  );
 
-# Now you can issue commands, for example:
-logvols = g.lvs ()
+  if native then pr "_%s " name else pr "%s " name;
+  pr "(";
+  let needs_comma = ref false in
+  if native then (
+    pr "long g";
+    needs_comma := true
+  );
 
-\"\"\"
+  (* args *)
+  List.iter (
+    fun arg ->
+      if !needs_comma then pr ", ";
+      needs_comma := true;
 
-import libguestfsmod
+      match arg with
+      | String n
+      | OptString n
+      | FileIn n
+      | FileOut n ->
+         pr "String %s" n
+      | StringList n ->
+         pr "String[] %s" n
+      | Bool n ->
+         pr "boolean %s" n
+      | Int n ->
+         pr "int %s" n
+  ) (snd style);
 
-class GuestFS:
-    \"\"\"Instances of this class are libguestfs API handles.\"\"\"
+  pr ")\n";
+  pr "    throws LibGuestFSException";
+  if semicolon then pr ";"
 
-    def __init__ (self):
-        \"\"\"Create a new libguestfs handle.\"\"\"
-        self._o = libguestfsmod.create ()
+and generate_java_struct typ cols =
+  generate_header CStyle LGPLv2;
 
-    def __del__ (self):
-        libguestfsmod.close (self._o)
+  pr "\
+package com.redhat.et.libguestfs;
 
-";
+/**
+ * Libguestfs %s structure.
+ *
+ * @author rjones
+ * @see GuestFS
+ */
+public class %s {
+" typ typ;
 
   List.iter (
-    fun (name, style, _, flags, _, _, longdesc) ->
-      let doc = replace_str longdesc "C<guestfs_" "C<g." in
-      let doc =
-        match fst style with
-       | RErr | RInt _ | RInt64 _ | RBool _ | RConstString _
-       | RString _ -> doc
-       | RStringList _ ->
-           doc ^ "\n\nThis function returns a list of strings."
-       | RIntBool _ ->
-           doc ^ "\n\nThis function returns a tuple (int, bool).\n"
-       | RPVList _ ->
-           doc ^ "\n\nThis function returns a list of PVs.  Each PV is represented as a dictionary."
-       | RVGList _ ->
-           doc ^ "\n\nThis function returns a list of VGs.  Each VG is represented as a dictionary."
-       | RLVList _ ->
-           doc ^ "\n\nThis function returns a list of LVs.  Each LV is represented as a dictionary."
-       | RStat _ ->
-           doc ^ "\n\nThis function returns a dictionary, with keys matching the various fields in the stat structure."
-       | RStatVFS _ ->
-           doc ^ "\n\nThis function returns a dictionary, with keys matching the various fields in the statvfs structure."
-       | RHashtable _ ->
-           doc ^ "\n\nThis function returns a dictionary." in
-      let doc =
-       if List.mem ProtocolLimitWarning flags then
-         doc ^ "\n\n" ^ protocol_limit_warning
-       else doc in
-      let doc =
-       if List.mem DangerWillRobinson flags then
-         doc ^ "\n\n" ^ danger_will_robinson
-       else doc in
-      let doc = pod2text ~width:60 name doc in
-      let doc = List.map (fun line -> replace_str line "\\" "\\\\") doc in
-      let doc = String.concat "\n        " doc in
-
-      pr "    def %s " name;
-      generate_call_args ~handle:"self" (snd style);
-      pr ":\n";
-      pr "        u\"\"\"%s\"\"\"\n" doc;
-      pr "        return libguestfsmod.%s " name;
-      generate_call_args ~handle:"self._o" (snd style);
-      pr "\n";
-      pr "\n";
-  ) all_functions
+    function
+    | name, `String
+    | name, `UUID -> pr "  public String %s;\n" name
+    | name, `Bytes
+    | name, `Int -> pr "  public long %s;\n" name
+    | name, `OptPercent ->
+       pr "  /* The next field is [0..100] or -1 meaning 'not present': */\n";
+       pr "  public float %s;\n" name
+  ) cols;
 
-(* Useful if you need the longdesc POD text as plain text.  Returns a
- * list of lines.
- *)
-and pod2text ~width name longdesc =
-  let filename, chan = Filename.open_temp_file "gen" ".tmp" in
-  fprintf chan "=head1 %s\n\n%s\n" name longdesc;
-  close_out chan;
-  let cmd = sprintf "pod2text -w %d %s" width (Filename.quote filename) in
-  let chan = Unix.open_process_in cmd in
-  let lines = ref [] in
-  let rec loop i =
-    let line = input_line chan in
-    if i = 1 then              (* discard the first line of output *)
-      loop (i+1)
-    else (
-      let line = triml line in
-      lines := line :: !lines;
-      loop (i+1)
-    ) in
-  let lines = try loop 1 with End_of_file -> List.rev !lines in
-  Unix.unlink filename;
-  match Unix.close_process_in chan with
-  | Unix.WEXITED 0 -> lines
-  | Unix.WEXITED i ->
-      failwithf "pod2text: process exited with non-zero status (%d)" i
-  | Unix.WSIGNALED i | Unix.WSTOPPED i ->
-      failwithf "pod2text: process signalled or stopped by signal %d" i
+  pr "}\n"
 
-(* Generate ruby bindings. *)
-and generate_ruby_c () =
+and generate_java_c () =
   generate_header CStyle LGPLv2;
 
   pr "\
 #include <stdio.h>
 #include <stdlib.h>
+#include <string.h>
 
-#include <ruby.h>
-
+#include \"com_redhat_et_libguestfs_GuestFS.h\"
 #include \"guestfs.h\"
 
-#include \"extconf.h\"
-
-static VALUE m_guestfs;                        /* guestfs module */
-static VALUE c_guestfs;                        /* guestfs_h handle */
-static VALUE e_Error;                  /* used for all errors */
-
-static void ruby_guestfs_free (void *p)
+/* Note that this function returns.  The exception is not thrown
+ * until after the wrapper function returns.
+ */
+static void
+throw_exception (JNIEnv *env, const char *msg)
 {
-  if (!p) return;
-  guestfs_close ((guestfs_h *) p);
+  jclass cl;
+  cl = (*env)->FindClass (env,
+                          \"com/redhat/et/libguestfs/LibGuestFSException\");
+  (*env)->ThrowNew (env, cl, msg);
 }
 
-static VALUE ruby_guestfs_create (VALUE m)
+JNIEXPORT jlong JNICALL
+Java_com_redhat_et_libguestfs_GuestFS__1create
+  (JNIEnv *env, jobject obj)
 {
   guestfs_h *g;
 
   g = guestfs_create ();
-  if (!g)
-    rb_raise (e_Error, \"failed to create guestfs handle\");
-
-  /* Don't print error messages to stderr by default. */
+  if (g == NULL) {
+    throw_exception (env, \"GuestFS.create: failed to allocate handle\");
+    return 0;
+  }
   guestfs_set_error_handler (g, NULL, NULL);
-
-  /* Wrap it, and make sure the close function is called when the
-   * handle goes away.
-   */
-  return Data_Wrap_Struct (c_guestfs, NULL, ruby_guestfs_free, g);
+  return (jlong) (long) g;
 }
 
-static VALUE ruby_guestfs_close (VALUE gv)
+JNIEXPORT void JNICALL
+Java_com_redhat_et_libguestfs_GuestFS__1close
+  (JNIEnv *env, jobject obj, jlong jg)
 {
-  guestfs_h *g;
-  Data_Get_Struct (gv, guestfs_h, g);
-
-  ruby_guestfs_free (g);
-  DATA_PTR (gv) = NULL;
-
-  return Qnil;
+  guestfs_h *g = (guestfs_h *) (long) jg;
+  guestfs_close (g);
 }
 
 ";
 
   List.iter (
     fun (name, style, _, _, _, _, _) ->
-      pr "static VALUE ruby_guestfs_%s (VALUE gv" name;
-      List.iter (fun arg -> pr ", VALUE %sv" (name_of_argt arg)) (snd style);
+      pr "JNIEXPORT ";
+      (match fst style with
+       | RErr -> pr "void ";
+       | RInt _ -> pr "jint ";
+       | RInt64 _ -> pr "jlong ";
+       | RBool _ -> pr "jboolean ";
+       | RConstString _ | RString _ -> pr "jstring ";
+       | RIntBool _ | RStat _ | RStatVFS _ | RHashtable _ ->
+          pr "jobject ";
+       | RStringList _ | RPVList _ | RVGList _ | RLVList _ ->
+          pr "jobjectArray ";
+      );
+      pr "JNICALL\n";
+      pr "Java_com_redhat_et_libguestfs_GuestFS_";
+      pr "%s" (replace_str ("_" ^ name) "_" "_1");
+      pr "\n";
+      pr "  (JNIEnv *env, jobject obj, jlong jg";
+      List.iter (
+       function
+       | String n
+       | OptString n
+       | FileIn n
+       | FileOut n ->
+           pr ", jstring j%s" n
+       | StringList n ->
+           pr ", jobjectArray j%s" n
+       | Bool n ->
+           pr ", jboolean j%s" n
+       | Int n ->
+           pr ", jint j%s" n
+      ) (snd style);
       pr ")\n";
       pr "{\n";
-      pr "  guestfs_h *g;\n";
-      pr "  Data_Get_Struct (gv, guestfs_h, g);\n";
-      pr "  if (!g)\n";
-      pr "    rb_raise (rb_eArgError, \"%%s: used handle after closing it\", \"%s\");\n"
-       name;
+      pr "  guestfs_h *g = (guestfs_h *) (long) jg;\n";
+      let error_code, no_ret =
+       match fst style with
+       | RErr -> pr "  int r;\n"; "-1", ""
+       | RBool _
+       | RInt _ -> pr "  int r;\n"; "-1", "0"
+       | RInt64 _ -> pr "  int64_t r;\n"; "-1", "0"
+       | RConstString _ -> pr "  const char *r;\n"; "NULL", "NULL"
+       | RString _ ->
+           pr "  jstring jr;\n";
+           pr "  char *r;\n"; "NULL", "NULL"
+       | RStringList _ ->
+           pr "  jobjectArray jr;\n";
+           pr "  int r_len;\n";
+           pr "  jclass cl;\n";
+           pr "  jstring jstr;\n";
+           pr "  char **r;\n"; "NULL", "NULL"
+       | RIntBool _ ->
+           pr "  jobject jr;\n";
+           pr "  jclass cl;\n";
+           pr "  jfieldID fl;\n";
+           pr "  struct guestfs_int_bool *r;\n"; "NULL", "NULL"
+       | RStat _ ->
+           pr "  jobject jr;\n";
+           pr "  jclass cl;\n";
+           pr "  jfieldID fl;\n";
+           pr "  struct guestfs_stat *r;\n"; "NULL", "NULL"
+       | RStatVFS _ ->
+           pr "  jobject jr;\n";
+           pr "  jclass cl;\n";
+           pr "  jfieldID fl;\n";
+           pr "  struct guestfs_statvfs *r;\n"; "NULL", "NULL"
+       | RPVList _ ->
+           pr "  jobjectArray jr;\n";
+           pr "  jclass cl;\n";
+           pr "  jfieldID fl;\n";
+           pr "  jobject jfl;\n";
+           pr "  struct guestfs_lvm_pv_list *r;\n"; "NULL", "NULL"
+       | RVGList _ ->
+           pr "  jobjectArray jr;\n";
+           pr "  jclass cl;\n";
+           pr "  jfieldID fl;\n";
+           pr "  jobject jfl;\n";
+           pr "  struct guestfs_lvm_vg_list *r;\n"; "NULL", "NULL"
+       | RLVList _ ->
+           pr "  jobjectArray jr;\n";
+           pr "  jclass cl;\n";
+           pr "  jfieldID fl;\n";
+           pr "  jobject jfl;\n";
+           pr "  struct guestfs_lvm_lv_list *r;\n"; "NULL", "NULL"
+       | RHashtable _ -> pr "  char **r;\n"; "NULL", "NULL" in
+      List.iter (
+       function
+       | String n
+       | OptString n
+       | FileIn n
+       | FileOut n ->
+           pr "  const char *%s;\n" n
+       | StringList n ->
+           pr "  int %s_len;\n" n;
+           pr "  const char **%s;\n" n
+       | Bool n
+       | Int n ->
+           pr "  int %s;\n" n
+      ) (snd style);
+
+      let needs_i =
+       (match fst style with
+        | RStringList _ | RPVList _ | RVGList _ | RLVList _ -> true
+        | RErr | RBool _ | RInt _ | RInt64 _ | RConstString _
+        | RString _ | RIntBool _ | RStat _ | RStatVFS _
+        | RHashtable _ -> false) ||
+       List.exists (function StringList _ -> true | _ -> false) (snd style) in
+      if needs_i then
+       pr "  int i;\n";
+
       pr "\n";
 
+      (* Get the parameters. *)
       List.iter (
        function
-       | String n | FileIn n | FileOut n ->
-           pr "  const char *%s = StringValueCStr (%sv);\n" n n;
-           pr "  if (!%s)\n" n;
-           pr "    rb_raise (rb_eTypeError, \"expected string for parameter %%s of %%s\",\n";
-           pr "              \"%s\", \"%s\");\n" n name
+       | String n
+       | FileIn n
+       | FileOut n ->
+           pr "  %s = (*env)->GetStringUTFChars (env, j%s, NULL);\n" n n
        | OptString n ->
-           pr "  const char *%s = StringValueCStr (%sv);\n" n n
+           (* This is completely undocumented, but Java null becomes
+            * a NULL parameter.
+            *)
+           pr "  %s = j%s ? (*env)->GetStringUTFChars (env, j%s, NULL) : NULL;\n" n n n
        | StringList n ->
-           pr "  char **%s;" n;
-           pr "  {\n";
-           pr "    int i, len;\n";
-           pr "    len = RARRAY_LEN (%sv);\n" n;
-           pr "    %s = malloc (sizeof (char *) * (len+1));\n" n;
-           pr "    for (i = 0; i < len; ++i) {\n";
-           pr "      VALUE v = rb_ary_entry (%sv, i);\n" n;
-           pr "      %s[i] = StringValueCStr (v);\n" n;
-           pr "    }\n";
+           pr "  %s_len = (*env)->GetArrayLength (env, j%s);\n" n n;
+           pr "  %s = guestfs_safe_malloc (g, sizeof (char *) * (%s_len+1));\n" n n;
+           pr "  for (i = 0; i < %s_len; ++i) {\n" n;
+           pr "    jobject o = (*env)->GetObjectArrayElement (env, j%s, i);\n"
+             n;
+           pr "    %s[i] = (*env)->GetStringUTFChars (env, o, NULL);\n" n;
            pr "  }\n";
+           pr "  %s[%s_len] = NULL;\n" n n;
        | Bool n
        | Int n ->
-           pr "  int %s = NUM2INT (%sv);\n" n n
+           pr "  %s = j%s;\n" n n
       ) (snd style);
-      pr "\n";
-
-      let error_code =
-       match fst style with
-       | RErr | RInt _ | RBool _ -> pr "  int r;\n"; "-1"
-       | RInt64 _ -> pr "  int64_t r;\n"; "-1"
-       | RConstString _ -> pr "  const char *r;\n"; "NULL"
-       | RString _ -> pr "  char *r;\n"; "NULL"
-       | RStringList _ | RHashtable _ -> pr "  char **r;\n"; "NULL"
-       | RIntBool _ -> pr "  struct guestfs_int_bool *r;\n"; "NULL"
-       | RPVList n -> pr "  struct guestfs_lvm_pv_list *r;\n"; "NULL"
-       | RVGList n -> pr "  struct guestfs_lvm_vg_list *r;\n"; "NULL"
-       | RLVList n -> pr "  struct guestfs_lvm_lv_list *r;\n"; "NULL"
-       | RStat n -> pr "  struct guestfs_stat *r;\n"; "NULL"
-       | RStatVFS n -> pr "  struct guestfs_statvfs *r;\n"; "NULL" in
-      pr "\n";
 
+      (* Make the call. *)
       pr "  r = guestfs_%s " name;
       generate_call_args ~handle:"g" (snd style);
       pr ";\n";
 
+      (* Release the parameters. *)
       List.iter (
        function
-       | String _ | FileIn _ | FileOut _ | OptString _ | Bool _ | Int _ -> ()
+       | String n
+       | FileIn n
+       | FileOut n ->
+           pr "  (*env)->ReleaseStringUTFChars (env, j%s, %s);\n" n n
+       | OptString n ->
+           pr "  if (j%s)\n" n;
+           pr "    (*env)->ReleaseStringUTFChars (env, j%s, %s);\n" n n
        | StringList n ->
+           pr "  for (i = 0; i < %s_len; ++i) {\n" n;
+           pr "    jobject o = (*env)->GetObjectArrayElement (env, j%s, i);\n"
+             n;
+           pr "    (*env)->ReleaseStringUTFChars (env, o, %s[i]);\n" n;
+           pr "  }\n";
            pr "  free (%s);\n" n
+       | Bool n
+       | Int n -> ()
       ) (snd style);
 
-      pr "  if (r == %s)\n" error_code;
-      pr "    rb_raise (e_Error, \"%%s\", guestfs_last_error (g));\n";
-      pr "\n";
+      (* Check for errors. *)
+      pr "  if (r == %s) {\n" error_code;
+      pr "    throw_exception (env, guestfs_last_error (g));\n";
+      pr "    return %s;\n" no_ret;
+      pr "  }\n";
 
+      (* Return value. *)
       (match fst style with
-       | RErr ->
-          pr "  return Qnil;\n"
-       | RInt _ | RBool _ ->
-          pr "  return INT2NUM (r);\n"
-       | RInt64 _ ->
-          pr "  return ULL2NUM (r);\n"
-       | RConstString _ ->
-          pr "  return rb_str_new2 (r);\n";
+       | RErr -> ()
+       | RInt _ -> pr "  return (jint) r;\n"
+       | RBool _ -> pr "  return (jboolean) r;\n"
+       | RInt64 _ -> pr "  return (jlong) r;\n"
+       | RConstString _ -> pr "  return (*env)->NewStringUTF (env, r);\n"
        | RString _ ->
-          pr "  VALUE rv = rb_str_new2 (r);\n";
+          pr "  jr = (*env)->NewStringUTF (env, r);\n";
           pr "  free (r);\n";
-          pr "  return rv;\n";
+          pr "  return jr;\n"
        | RStringList _ ->
-          pr "  int i, len = 0;\n";
-          pr "  for (i = 0; r[i] != NULL; ++i) len++;\n";
-          pr "  VALUE rv = rb_ary_new2 (len);\n";
-          pr "  for (i = 0; r[i] != NULL; ++i) {\n";
-          pr "    rb_ary_push (rv, rb_str_new2 (r[i]));\n";
+          pr "  for (r_len = 0; r[r_len] != NULL; ++r_len) ;\n";
+          pr "  cl = (*env)->FindClass (env, \"java/lang/String\");\n";
+          pr "  jstr = (*env)->NewStringUTF (env, \"\");\n";
+          pr "  jr = (*env)->NewObjectArray (env, r_len, cl, jstr);\n";
+          pr "  for (i = 0; i < r_len; ++i) {\n";
+          pr "    jstr = (*env)->NewStringUTF (env, r[i]);\n";
+          pr "    (*env)->SetObjectArrayElement (env, jr, i, jstr);\n";
           pr "    free (r[i]);\n";
           pr "  }\n";
-          pr "  free (r);\n";
-          pr "  return rv;\n"
-       | RIntBool _ ->
-          pr "  VALUE rv = rb_ary_new2 (2);\n";
-          pr "  rb_ary_push (rv, INT2NUM (r->i));\n";
-          pr "  rb_ary_push (rv, INT2NUM (r->b));\n";
+          pr "  free (r);\n";
+          pr "  return jr;\n"
+       | RIntBool _ ->
+          pr "  cl = (*env)->FindClass (env, \"com/redhat/et/libguestfs/IntBool\");\n";
+          pr "  jr = (*env)->AllocObject (env, cl);\n";
+          pr "  fl = (*env)->GetFieldID (env, cl, \"i\", \"I\");\n";
+          pr "  (*env)->SetIntField (env, jr, fl, r->i);\n";
+          pr "  fl = (*env)->GetFieldID (env, cl, \"i\", \"Z\");\n";
+          pr "  (*env)->SetBooleanField (env, jr, fl, r->b);\n";
           pr "  guestfs_free_int_bool (r);\n";
-          pr "  return rv;\n"
-       | RPVList n ->
-          generate_ruby_lvm_code "pv" pv_cols
-       | RVGList n ->
-          generate_ruby_lvm_code "vg" vg_cols
-       | RLVList n ->
-          generate_ruby_lvm_code "lv" lv_cols
-       | RStat n ->
-          pr "  VALUE rv = rb_hash_new ();\n";
+          pr "  return jr;\n"
+       | RStat _ ->
+          pr "  cl = (*env)->FindClass (env, \"com/redhat/et/libguestfs/Stat\");\n";
+          pr "  jr = (*env)->AllocObject (env, cl);\n";
           List.iter (
             function
             | name, `Int ->
-                pr "  rb_hash_aset (rv, rb_str_new2 (\"%s\"), ULL2NUM (r->%s));\n" name name
+                pr "  fl = (*env)->GetFieldID (env, cl, \"%s\", \"J\");\n"
+                  name;
+                pr "  (*env)->SetLongField (env, jr, fl, r->%s);\n" name;
           ) stat_cols;
           pr "  free (r);\n";
-          pr "  return rv;\n"
-       | RStatVFS n ->
-          pr "  VALUE rv = rb_hash_new ();\n";
+          pr "  return jr;\n"
+       | RStatVFS _ ->
+          pr "  cl = (*env)->FindClass (env, \"com/redhat/et/libguestfs/StatVFS\");\n";
+          pr "  jr = (*env)->AllocObject (env, cl);\n";
           List.iter (
             function
             | name, `Int ->
-                pr "  rb_hash_aset (rv, rb_str_new2 (\"%s\"), ULL2NUM (r->%s));\n" name name
+                pr "  fl = (*env)->GetFieldID (env, cl, \"%s\", \"J\");\n"
+                  name;
+                pr "  (*env)->SetLongField (env, jr, fl, r->%s);\n" name;
           ) statvfs_cols;
           pr "  free (r);\n";
-          pr "  return rv;\n"
+          pr "  return jr;\n"
+       | RPVList _ ->
+          generate_java_lvm_return "pv" "PV" pv_cols
+       | RVGList _ ->
+          generate_java_lvm_return "vg" "VG" vg_cols
+       | RLVList _ ->
+          generate_java_lvm_return "lv" "LV" lv_cols
        | RHashtable _ ->
-          pr "  VALUE rv = rb_hash_new ();\n";
-          pr "  int i;\n";
-          pr "  for (i = 0; r[i] != NULL; i+=2) {\n";
-          pr "    rb_hash_aset (rv, rb_str_new2 (r[i]), rb_str_new2 (r[i+1]));\n";
-          pr "    free (r[i]);\n";
-          pr "    free (r[i+1]);\n";
-          pr "  }\n";
-          pr "  free (r);\n";
-          pr "  return rv;\n"
+          (* XXX *)
+          pr "  throw_exception (env, \"%s: internal error: please let us know how to make a Java HashMap from JNI bindings!\");\n" name;
+          pr "  return NULL;\n"
       );
 
       pr "}\n";
       pr "\n"
+  ) all_functions
+
+and generate_java_lvm_return typ jtyp cols =
+  pr "  cl = (*env)->FindClass (env, \"com/redhat/et/libguestfs/%s\");\n" jtyp;
+  pr "  jr = (*env)->NewObjectArray (env, r->len, cl, NULL);\n";
+  pr "  for (i = 0; i < r->len; ++i) {\n";
+  pr "    jfl = (*env)->AllocObject (env, cl);\n";
+  List.iter (
+    function
+    | name, `String ->
+       pr "    fl = (*env)->GetFieldID (env, cl, \"%s\", \"Ljava/lang/String;\");\n" name;
+       pr "    (*env)->SetObjectField (env, jfl, fl, (*env)->NewStringUTF (env, r->val[i].%s));\n" name;
+    | name, `UUID ->
+       pr "    {\n";
+       pr "      char s[33];\n";
+       pr "      memcpy (s, r->val[i].%s, 32);\n" name;
+       pr "      s[32] = 0;\n";
+       pr "      fl = (*env)->GetFieldID (env, cl, \"%s\", \"Ljava/lang/String;\");\n" name;
+       pr "      (*env)->SetObjectField (env, jfl, fl, (*env)->NewStringUTF (env, s));\n";
+       pr "    }\n";
+    | name, (`Bytes|`Int) ->
+       pr "    fl = (*env)->GetFieldID (env, cl, \"%s\", \"J\");\n" name;
+       pr "    (*env)->SetLongField (env, jfl, fl, r->val[i].%s);\n" name;
+    | name, `OptPercent ->
+       pr "    fl = (*env)->GetFieldID (env, cl, \"%s\", \"F\");\n" name;
+       pr "    (*env)->SetFloatField (env, jfl, fl, r->val[i].%s);\n" name;
+  ) cols;
+  pr "    (*env)->SetObjectArrayElement (env, jfl, i, jfl);\n";
+  pr "  }\n";
+  pr "  guestfs_free_lvm_%s_list (r);\n" typ;
+  pr "  return jr;\n"
+
+and generate_haskell_hs () =
+  generate_header HaskellStyle LGPLv2;
+
+  (* XXX We only know how to generate partial FFI for Haskell
+   * at the moment.  Please help out!
+   *)
+  let can_generate style =
+    match style with
+    | RErr, _
+    | RInt _, _
+    | RInt64 _, _ -> true
+    | RBool _, _
+    | RConstString _, _
+    | RString _, _
+    | RStringList _, _
+    | RIntBool _, _
+    | RPVList _, _
+    | RVGList _, _
+    | RLVList _, _
+    | RStat _, _
+    | RStatVFS _, _
+    | RHashtable _, _ -> false in
+
+  pr "\
+{-# INCLUDE <guestfs.h> #-}
+{-# LANGUAGE ForeignFunctionInterface #-}
+
+module Guestfs (
+  create";
+
+  (* List out the names of the actions we want to export. *)
+  List.iter (
+    fun (name, style, _, _, _, _, _) ->
+      if can_generate style then pr ",\n  %s" name
   ) all_functions;
 
+  pr "
+  ) where
+import Foreign
+import Foreign.C
+import Foreign.C.Types
+import IO
+import Control.Exception
+import Data.Typeable
+
+data GuestfsS = GuestfsS            -- represents the opaque C struct
+type GuestfsP = Ptr GuestfsS        -- guestfs_h *
+type GuestfsH = ForeignPtr GuestfsS -- guestfs_h * with attached finalizer
+
+-- XXX define properly later XXX
+data PV = PV
+data VG = VG
+data LV = LV
+data IntBool = IntBool
+data Stat = Stat
+data StatVFS = StatVFS
+data Hashtable = Hashtable
+
+foreign import ccall unsafe \"guestfs_create\" c_create
+  :: IO GuestfsP
+foreign import ccall unsafe \"&guestfs_close\" c_close
+  :: FunPtr (GuestfsP -> IO ())
+foreign import ccall unsafe \"guestfs_set_error_handler\" c_set_error_handler
+  :: GuestfsP -> Ptr CInt -> Ptr CInt -> IO ()
+
+create :: IO GuestfsH
+create = do
+  p <- c_create
+  c_set_error_handler p nullPtr nullPtr
+  h <- newForeignPtr c_close p
+  return h
+
+foreign import ccall unsafe \"guestfs_last_error\" c_last_error
+  :: GuestfsP -> IO CString
+
+-- last_error :: GuestfsH -> IO (Maybe String)
+-- last_error h = do
+--   str <- withForeignPtr h (\\p -> c_last_error p)
+--   maybePeek peekCString str
+
+last_error :: GuestfsH -> IO (String)
+last_error h = do
+  str <- withForeignPtr h (\\p -> c_last_error p)
+  if (str == nullPtr)
+    then return \"no error\"
+    else peekCString str
+
+";
+
+  (* Generate wrappers for each foreign function. *)
+  List.iter (
+    fun (name, style, _, _, _, _, _) ->
+      if can_generate style then (
+       pr "foreign import ccall unsafe \"guestfs_%s\" c_%s\n" name name;
+       pr "  :: ";
+       generate_haskell_prototype ~handle:"GuestfsP" style;
+       pr "\n";
+       pr "\n";
+       pr "%s :: " name;
+       generate_haskell_prototype ~handle:"GuestfsH" ~hs:true style;
+       pr "\n";
+       pr "%s %s = do\n" name
+         (String.concat " " ("h" :: List.map name_of_argt (snd style)));
+       pr "  r <- ";
+       (* Convert pointer arguments using with* functions. *)
+       List.iter (
+         function
+         | FileIn n
+         | FileOut n
+         | String n -> pr "withCString %s $ \\%s -> " n n
+         | OptString n -> pr "maybeWith withCString %s $ \\%s -> " n n
+         | StringList n -> pr "withMany withCString %s $ \\%s -> withArray0 nullPtr %s $ \\%s -> " n n n n
+         | Bool _ | Int _ -> ()
+       ) (snd style);
+       (* Convert integer arguments. *)
+       let args =
+         List.map (
+           function
+           | Bool n -> sprintf "(fromBool %s)" n
+           | Int n -> sprintf "(fromIntegral %s)" n
+           | FileIn n | FileOut n | String n | OptString n | StringList n -> n
+         ) (snd style) in
+       pr "withForeignPtr h (\\p -> c_%s %s)\n" name
+         (String.concat " " ("p" :: args));
+       (match fst style with
+        | RErr | RInt _ | RInt64 _ | RBool _ ->
+            pr "  if (r == -1)\n";
+            pr "    then do\n";
+            pr "      err <- last_error h\n";
+            pr "      fail err\n";
+        | RConstString _ | RString _ | RStringList _ | RIntBool _
+        | RPVList _ | RVGList _ | RLVList _ | RStat _ | RStatVFS _
+        | RHashtable _ ->
+            pr "  if (r == nullPtr)\n";
+            pr "    then do\n";
+            pr "      err <- last_error h\n";
+            pr "      fail err\n";
+       );
+       (match fst style with
+        | RErr ->
+            pr "    else return ()\n"
+        | RInt _ ->
+            pr "    else return (fromIntegral r)\n"
+        | RInt64 _ ->
+            pr "    else return (fromIntegral r)\n"
+        | RBool _ ->
+            pr "    else return (toBool r)\n"
+        | RConstString _
+        | RString _
+        | RStringList _
+        | RIntBool _
+        | RPVList _
+        | RVGList _
+        | RLVList _
+        | RStat _
+        | RStatVFS _
+        | RHashtable _ ->
+            pr "    else return ()\n" (* XXXXXXXXXXXXXXXXXXXX *)
+       );
+       pr "\n";
+      )
+  ) all_functions
+
+and generate_haskell_prototype ~handle ?(hs = false) style =
+  pr "%s -> " handle;
+  let string = if hs then "String" else "CString" in
+  let int = if hs then "Int" else "CInt" in
+  let bool = if hs then "Bool" else "CInt" in
+  let int64 = if hs then "Integer" else "Int64" in
+  List.iter (
+    fun arg ->
+      (match arg with
+       | String _ -> pr "%s" string
+       | OptString _ -> if hs then pr "Maybe String" else pr "CString"
+       | StringList _ -> if hs then pr "[String]" else pr "Ptr CString"
+       | Bool _ -> pr "%s" bool
+       | Int _ -> pr "%s" int
+       | FileIn _ -> pr "%s" string
+       | FileOut _ -> pr "%s" string
+      );
+      pr " -> ";
+  ) (snd style);
+  pr "IO (";
+  (match fst style with
+   | RErr -> if not hs then pr "CInt"
+   | RInt _ -> pr "%s" int
+   | RInt64 _ -> pr "%s" int64
+   | RBool _ -> pr "%s" bool
+   | RConstString _ -> pr "%s" string
+   | RString _ -> pr "%s" string
+   | RStringList _ -> pr "[%s]" string
+   | RIntBool _ -> pr "IntBool"
+   | RPVList _ -> pr "[PV]"
+   | RVGList _ -> pr "[VG]"
+   | RLVList _ -> pr "[LV]"
+   | RStat _ -> pr "Stat"
+   | RStatVFS _ -> pr "StatVFS"
+   | RHashtable _ -> pr "Hashtable"
+  );
+  pr ")"
+
+and generate_bindtests () =
+  generate_header CStyle LGPLv2;
+
   pr "\
-/* Initialize the module. */
-void Init__guestfs ()
+#include <stdio.h>
+#include <stdlib.h>
+#include <inttypes.h>
+#include <string.h>
+
+#include \"guestfs.h\"
+#include \"guestfs_protocol.h\"
+
+#define error guestfs_error
+
+static void
+print_strings (char * const* const argv)
 {
-  m_guestfs = rb_define_module (\"Guestfs\");
-  c_guestfs = rb_define_class_under (m_guestfs, \"Guestfs\", rb_cObject);
-  e_Error = rb_define_class_under (m_guestfs, \"Error\", rb_eStandardError);
+  int argc;
 
-  rb_define_module_function (m_guestfs, \"create\", ruby_guestfs_create, 0);
-  rb_define_method (c_guestfs, \"close\", ruby_guestfs_close, 0);
+  printf (\"[\");
+  for (argc = 0; argv[argc] != NULL; ++argc) {
+    if (argc > 0) printf (\", \");
+    printf (\"\\\"%%s\\\"\", argv[argc]);
+  }
+  printf (\"]\\n\");
+}
 
+/* The test0 function prints its parameters to stdout. */
 ";
-  (* Define the rest of the methods. *)
+
+  let test0, tests =
+    match test_functions with
+    | [] -> assert false
+    | test0 :: tests -> test0, tests in
+
+  let () =
+    let (name, style, _, _, _, _, _) = test0 in
+    generate_prototype ~extern:false ~semicolon:false ~newline:true
+      ~handle:"g" ~prefix:"guestfs_" name style;
+    pr "{\n";
+    List.iter (
+      function
+      | String n
+      | FileIn n
+      | FileOut n -> pr "  printf (\"%%s\\n\", %s);\n" n
+      | OptString n -> pr "  printf (\"%%s\\n\", %s ? %s : \"null\");\n" n n
+      | StringList n -> pr "  print_strings (%s);\n" n
+      | Bool n -> pr "  printf (\"%%s\\n\", %s ? \"true\" : \"false\");\n" n
+      | Int n -> pr "  printf (\"%%d\\n\", %s);\n" n
+    ) (snd style);
+    pr "  /* Java changes stdout line buffering so we need this: */\n";
+    pr "  fflush (stdout);\n";
+    pr "  return 0;\n";
+    pr "}\n";
+    pr "\n" in
+
   List.iter (
     fun (name, style, _, _, _, _, _) ->
-      pr "  rb_define_method (c_guestfs, \"%s\",\n" name;
-      pr "        ruby_guestfs_%s, %d);\n" name (List.length (snd style))
-  ) all_functions;
+      if String.sub name (String.length name - 3) 3 <> "err" then (
+       pr "/* Test normal return. */\n";
+       generate_prototype ~extern:false ~semicolon:false ~newline:true
+         ~handle:"g" ~prefix:"guestfs_" name style;
+       pr "{\n";
+       (match fst style with
+        | RErr ->
+            pr "  return 0;\n"
+        | RInt _ ->
+            pr "  int r;\n";
+            pr "  sscanf (val, \"%%d\", &r);\n";
+            pr "  return r;\n"
+        | RInt64 _ ->
+            pr "  int64_t r;\n";
+            pr "  sscanf (val, \"%%\" SCNi64, &r);\n";
+            pr "  return r;\n"
+        | RBool _ ->
+            pr "  return strcmp (val, \"true\") == 0;\n"
+        | RConstString _ ->
+            (* Can't return the input string here.  Return a static
+             * string so we ensure we get a segfault if the caller
+             * tries to free it.
+             *)
+            pr "  return \"static string\";\n"
+        | RString _ ->
+            pr "  return strdup (val);\n"
+        | RStringList _ ->
+            pr "  char **strs;\n";
+            pr "  int n, i;\n";
+            pr "  sscanf (val, \"%%d\", &n);\n";
+            pr "  strs = malloc ((n+1) * sizeof (char *));\n";
+            pr "  for (i = 0; i < n; ++i) {\n";
+            pr "    strs[i] = malloc (16);\n";
+            pr "    snprintf (strs[i], 16, \"%%d\", i);\n";
+            pr "  }\n";
+            pr "  strs[n] = NULL;\n";
+            pr "  return strs;\n"
+        | RIntBool _ ->
+            pr "  struct guestfs_int_bool *r;\n";
+            pr "  r = malloc (sizeof (struct guestfs_int_bool));\n";
+            pr "  sscanf (val, \"%%\" SCNi32, &r->i);\n";
+            pr "  r->b = 0;\n";
+            pr "  return r;\n"
+        | RPVList _ ->
+            pr "  struct guestfs_lvm_pv_list *r;\n";
+            pr "  int i;\n";
+            pr "  r = malloc (sizeof (struct guestfs_lvm_pv_list));\n";
+            pr "  sscanf (val, \"%%d\", &r->len);\n";
+            pr "  r->val = calloc (r->len, sizeof (struct guestfs_lvm_pv));\n";
+            pr "  for (i = 0; i < r->len; ++i) {\n";
+            pr "    r->val[i].pv_name = malloc (16);\n";
+            pr "    snprintf (r->val[i].pv_name, 16, \"%%d\", i);\n";
+            pr "  }\n";
+            pr "  return r;\n"
+        | RVGList _ ->
+            pr "  struct guestfs_lvm_vg_list *r;\n";
+            pr "  int i;\n";
+            pr "  r = malloc (sizeof (struct guestfs_lvm_vg_list));\n";
+            pr "  sscanf (val, \"%%d\", &r->len);\n";
+            pr "  r->val = calloc (r->len, sizeof (struct guestfs_lvm_vg));\n";
+            pr "  for (i = 0; i < r->len; ++i) {\n";
+            pr "    r->val[i].vg_name = malloc (16);\n";
+            pr "    snprintf (r->val[i].vg_name, 16, \"%%d\", i);\n";
+            pr "  }\n";
+            pr "  return r;\n"
+        | RLVList _ ->
+            pr "  struct guestfs_lvm_lv_list *r;\n";
+            pr "  int i;\n";
+            pr "  r = malloc (sizeof (struct guestfs_lvm_lv_list));\n";
+            pr "  sscanf (val, \"%%d\", &r->len);\n";
+            pr "  r->val = calloc (r->len, sizeof (struct guestfs_lvm_lv));\n";
+            pr "  for (i = 0; i < r->len; ++i) {\n";
+            pr "    r->val[i].lv_name = malloc (16);\n";
+            pr "    snprintf (r->val[i].lv_name, 16, \"%%d\", i);\n";
+            pr "  }\n";
+            pr "  return r;\n"
+        | RStat _ ->
+            pr "  struct guestfs_stat *r;\n";
+            pr "  r = calloc (1, sizeof (*r));\n";
+            pr "  sscanf (val, \"%%\" SCNi64, &r->dev);\n";
+            pr "  return r;\n"
+        | RStatVFS _ ->
+            pr "  struct guestfs_statvfs *r;\n";
+            pr "  r = calloc (1, sizeof (*r));\n";
+            pr "  sscanf (val, \"%%\" SCNi64, &r->bsize);\n";
+            pr "  return r;\n"
+        | RHashtable _ ->
+            pr "  char **strs;\n";
+            pr "  int n, i;\n";
+            pr "  sscanf (val, \"%%d\", &n);\n";
+            pr "  strs = malloc ((n*2+1) * sizeof (char *));\n";
+            pr "  for (i = 0; i < n; ++i) {\n";
+            pr "    strs[i*2] = malloc (16);\n";
+            pr "    strs[i*2+1] = malloc (16);\n";
+            pr "    snprintf (strs[i*2], 16, \"%%d\", i);\n";
+            pr "    snprintf (strs[i*2+1], 16, \"%%d\", i);\n";
+            pr "  }\n";
+            pr "  strs[n*2] = NULL;\n";
+            pr "  return strs;\n"
+       );
+       pr "}\n";
+       pr "\n"
+      ) else (
+       pr "/* Test error return. */\n";
+       generate_prototype ~extern:false ~semicolon:false ~newline:true
+         ~handle:"g" ~prefix:"guestfs_" name style;
+       pr "{\n";
+       pr "  error (g, \"error\");\n";
+       (match fst style with
+        | RErr | RInt _ | RInt64 _ | RBool _ ->
+            pr "  return -1;\n"
+        | RConstString _
+        | RString _ | RStringList _ | RIntBool _
+        | RPVList _ | RVGList _ | RLVList _ | RStat _ | RStatVFS _
+        | RHashtable _ ->
+            pr "  return NULL;\n"
+       );
+       pr "}\n";
+       pr "\n"
+      )
+  ) tests
 
-  pr "}\n"
+and generate_ocaml_bindtests () =
+  generate_header OCamlStyle GPLv2;
 
-(* Ruby code to return an LVM struct list. *)
-and generate_ruby_lvm_code typ cols =
-  pr "  VALUE rv = rb_ary_new2 (r->len);\n";
-  pr "  int i;\n";
-  pr "  for (i = 0; i < r->len; ++i) {\n";
-  pr "    VALUE hv = rb_hash_new ();\n";
-  List.iter (
-    function
-    | name, `String ->
-       pr "    rb_hash_aset (rv, rb_str_new2 (\"%s\"), rb_str_new2 (r->val[i].%s));\n" name name
-    | name, `UUID ->
-       pr "    rb_hash_aset (rv, rb_str_new2 (\"%s\"), rb_str_new (r->val[i].%s, 32));\n" name name
-    | name, `Bytes
-    | name, `Int ->
-       pr "    rb_hash_aset (rv, rb_str_new2 (\"%s\"), ULL2NUM (r->val[i].%s));\n" name name
-    | name, `OptPercent ->
-       pr "    rb_hash_aset (rv, rb_str_new2 (\"%s\"), rb_dbl2big (r->val[i].%s));\n" name name
-  ) cols;
-  pr "    rb_ary_push (rv, hv);\n";
-  pr "  }\n";
-  pr "  guestfs_free_lvm_%s_list (r);\n" typ;
-  pr "  return rv;\n"
+  pr "\
+let () =
+  let g = Guestfs.create () in
+";
+
+  let mkargs args =
+    String.concat " " (
+      List.map (
+       function
+       | CallString s -> "\"" ^ s ^ "\""
+       | CallOptString None -> "None"
+       | CallOptString (Some s) -> sprintf "(Some \"%s\")" s
+       | CallStringList xs ->
+           "[|" ^ String.concat ";" (List.map (sprintf "\"%s\"") xs) ^ "|]"
+       | CallInt i when i >= 0 -> string_of_int i
+       | CallInt i (* when i < 0 *) -> "(" ^ string_of_int i ^ ")"
+       | CallBool b -> string_of_bool b
+      ) args
+    )
+  in
+
+  generate_lang_bindtests (
+    fun f args -> pr "  Guestfs.%s g %s;\n" f (mkargs args)
+  );
+
+  pr "print_endline \"EOF\"\n"
+
+and generate_perl_bindtests () =
+  pr "#!/usr/bin/perl -w\n";
+  generate_header HashStyle GPLv2;
+
+  pr "\
+use strict;
+
+use Sys::Guestfs;
+
+my $g = Sys::Guestfs->new ();
+";
+
+  let mkargs args =
+    String.concat ", " (
+      List.map (
+       function
+       | CallString s -> "\"" ^ s ^ "\""
+       | CallOptString None -> "undef"
+       | CallOptString (Some s) -> sprintf "\"%s\"" s
+       | CallStringList xs ->
+           "[" ^ String.concat "," (List.map (sprintf "\"%s\"") xs) ^ "]"
+       | CallInt i -> string_of_int i
+       | CallBool b -> if b then "1" else "0"
+      ) args
+    )
+  in
+
+  generate_lang_bindtests (
+    fun f args -> pr "$g->%s (%s);\n" f (mkargs args)
+  );
+
+  pr "print \"EOF\\n\"\n"
+
+and generate_python_bindtests () =
+  generate_header HashStyle GPLv2;
+
+  pr "\
+import guestfs
+
+g = guestfs.GuestFS ()
+";
+
+  let mkargs args =
+    String.concat ", " (
+      List.map (
+       function
+       | CallString s -> "\"" ^ s ^ "\""
+       | CallOptString None -> "None"
+       | CallOptString (Some s) -> sprintf "\"%s\"" s
+       | CallStringList xs ->
+           "[" ^ String.concat "," (List.map (sprintf "\"%s\"") xs) ^ "]"
+       | CallInt i -> string_of_int i
+       | CallBool b -> if b then "1" else "0"
+      ) args
+    )
+  in
+
+  generate_lang_bindtests (
+    fun f args -> pr "g.%s (%s)\n" f (mkargs args)
+  );
+
+  pr "print \"EOF\"\n"
+
+and generate_ruby_bindtests () =
+  generate_header HashStyle GPLv2;
+
+  pr "\
+require 'guestfs'
+
+g = Guestfs::create()
+";
+
+  let mkargs args =
+    String.concat ", " (
+      List.map (
+       function
+       | CallString s -> "\"" ^ s ^ "\""
+       | CallOptString None -> "nil"
+       | CallOptString (Some s) -> sprintf "\"%s\"" s
+       | CallStringList xs ->
+           "[" ^ String.concat "," (List.map (sprintf "\"%s\"") xs) ^ "]"
+       | CallInt i -> string_of_int i
+       | CallBool b -> string_of_bool b
+      ) args
+    )
+  in
+
+  generate_lang_bindtests (
+    fun f args -> pr "g.%s(%s)\n" f (mkargs args)
+  );
+
+  pr "print \"EOF\\n\"\n"
+
+and generate_java_bindtests () =
+  generate_header CStyle GPLv2;
+
+  pr "\
+import com.redhat.et.libguestfs.*;
+
+public class Bindtests {
+    public static void main (String[] argv)
+    {
+        try {
+            GuestFS g = new GuestFS ();
+";
+
+  let mkargs args =
+    String.concat ", " (
+      List.map (
+       function
+       | CallString s -> "\"" ^ s ^ "\""
+       | CallOptString None -> "null"
+       | CallOptString (Some s) -> sprintf "\"%s\"" s
+       | CallStringList xs ->
+           "new String[]{" ^
+             String.concat "," (List.map (sprintf "\"%s\"") xs) ^ "}"
+       | CallInt i -> string_of_int i
+       | CallBool b -> string_of_bool b
+      ) args
+    )
+  in
+
+  generate_lang_bindtests (
+    fun f args -> pr "            g.%s (%s);\n" f (mkargs args)
+  );
+
+  pr "
+            System.out.println (\"EOF\");
+        }
+        catch (Exception exn) {
+            System.err.println (exn);
+            System.exit (1);
+        }
+    }
+}
+"
+
+and generate_haskell_bindtests () =
+  generate_header HaskellStyle GPLv2;
+
+  pr "\
+module Bindtests where
+import qualified Guestfs
+
+main = do
+  g <- Guestfs.create
+";
+
+  let mkargs args =
+    String.concat " " (
+      List.map (
+       function
+       | CallString s -> "\"" ^ s ^ "\""
+       | CallOptString None -> "Nothing"
+       | CallOptString (Some s) -> sprintf "(Just \"%s\")" s
+       | CallStringList xs ->
+           "[" ^ String.concat "," (List.map (sprintf "\"%s\"") xs) ^ "]"
+       | CallInt i when i < 0 -> "(" ^ string_of_int i ^ ")"
+       | CallInt i -> string_of_int i
+       | CallBool true -> "True"
+       | CallBool false -> "False"
+      ) args
+    )
+  in
+
+  generate_lang_bindtests (
+    fun f args -> pr "  Guestfs.%s g %s\n" f (mkargs args)
+  );
+
+  pr "  putStrLn \"EOF\"\n"
+
+(* Language-independent bindings tests - we do it this way to
+ * ensure there is parity in testing bindings across all languages.
+ *)
+and generate_lang_bindtests call =
+  call "test0" [CallString "abc"; CallOptString (Some "def");
+               CallStringList []; CallBool false;
+               CallInt 0; CallString "123"; CallString "456"];
+  call "test0" [CallString "abc"; CallOptString None;
+               CallStringList []; CallBool false;
+               CallInt 0; CallString "123"; CallString "456"];
+  call "test0" [CallString ""; CallOptString (Some "def");
+               CallStringList []; CallBool false;
+               CallInt 0; CallString "123"; CallString "456"];
+  call "test0" [CallString ""; CallOptString (Some "");
+               CallStringList []; CallBool false;
+               CallInt 0; CallString "123"; CallString "456"];
+  call "test0" [CallString "abc"; CallOptString (Some "def");
+               CallStringList ["1"]; CallBool false;
+               CallInt 0; CallString "123"; CallString "456"];
+  call "test0" [CallString "abc"; CallOptString (Some "def");
+               CallStringList ["1"; "2"]; CallBool false;
+               CallInt 0; CallString "123"; CallString "456"];
+  call "test0" [CallString "abc"; CallOptString (Some "def");
+               CallStringList ["1"]; CallBool true;
+               CallInt 0; CallString "123"; CallString "456"];
+  call "test0" [CallString "abc"; CallOptString (Some "def");
+               CallStringList ["1"]; CallBool false;
+               CallInt (-1); CallString "123"; CallString "456"];
+  call "test0" [CallString "abc"; CallOptString (Some "def");
+               CallStringList ["1"]; CallBool false;
+               CallInt (-2); CallString "123"; CallString "456"];
+  call "test0" [CallString "abc"; CallOptString (Some "def");
+               CallStringList ["1"]; CallBool false;
+               CallInt 1; CallString "123"; CallString "456"];
+  call "test0" [CallString "abc"; CallOptString (Some "def");
+               CallStringList ["1"]; CallBool false;
+               CallInt 2; CallString "123"; CallString "456"];
+  call "test0" [CallString "abc"; CallOptString (Some "def");
+               CallStringList ["1"]; CallBool false;
+               CallInt 4095; CallString "123"; CallString "456"];
+  call "test0" [CallString "abc"; CallOptString (Some "def");
+               CallStringList ["1"]; CallBool false;
+               CallInt 0; CallString ""; CallString ""]
+
+  (* XXX Add here tests of the return and error functions. *)
+
+(* This is used to generate the src/MAX_PROC_NR file which
+ * contains the maximum procedure number, a surrogate for the
+ * ABI version number.  See src/Makefile.am for the details.
+ *)
+and generate_max_proc_nr () =
+  let proc_nrs = List.map (
+    fun (_, _, proc_nr, _, _, _, _) -> proc_nr
+  ) daemon_functions in
+
+  let max_proc_nr = List.fold_left max 0 proc_nrs in
+
+  pr "%d\n" max_proc_nr
 
 let output_to filename =
   let filename_new = filename ^ ".new" in
@@ -5347,8 +7940,17 @@ let output_to filename =
   let close () =
     close_out !chan;
     chan := stdout;
-    Unix.rename filename_new filename;
-    printf "written %s\n%!" filename;
+
+    (* Is the new file different from the current file? *)
+    if Sys.file_exists filename && files_equal filename filename_new then
+      Unix.unlink filename_new         (* same, so skip it *)
+    else (
+      (* different, overwrite old one *)
+      (try Unix.chmod filename 0o644 with Unix.Unix_error _ -> ());
+      Unix.rename filename_new filename;
+      Unix.chmod filename 0o444;
+      printf "written %s\n%!" filename;
+    )
   in
   close
 
@@ -5389,10 +7991,14 @@ Run it from the top source directory using the command
   generate_daemon_actions ();
   close ();
 
-  let close = output_to "tests.c" in
+  let close = output_to "capitests/tests.c" in
   generate_tests ();
   close ();
 
+  let close = output_to "src/guestfs-bindtests.c" in
+  generate_bindtests ();
+  close ();
+
   let close = output_to "fish/cmds.c" in
   generate_fish_cmds ();
   close ();
@@ -5425,6 +8031,10 @@ Run it from the top source directory using the command
   generate_ocaml_c ();
   close ();
 
+  let close = output_to "ocaml/bindtests.ml" in
+  generate_ocaml_bindtests ();
+  close ();
+
   let close = output_to "perl/Guestfs.xs" in
   generate_perl_xs ();
   close ();
@@ -5433,6 +8043,10 @@ Run it from the top source directory using the command
   generate_perl_pm ();
   close ();
 
+  let close = output_to "perl/bindtests.pl" in
+  generate_perl_bindtests ();
+  close ();
+
   let close = output_to "python/guestfs-py.c" in
   generate_python_c ();
   close ();
@@ -5441,6 +8055,58 @@ Run it from the top source directory using the command
   generate_python_py ();
   close ();
 
+  let close = output_to "python/bindtests.py" in
+  generate_python_bindtests ();
+  close ();
+
   let close = output_to "ruby/ext/guestfs/_guestfs.c" in
   generate_ruby_c ();
   close ();
+
+  let close = output_to "ruby/bindtests.rb" in
+  generate_ruby_bindtests ();
+  close ();
+
+  let close = output_to "java/com/redhat/et/libguestfs/GuestFS.java" in
+  generate_java_java ();
+  close ();
+
+  let close = output_to "java/com/redhat/et/libguestfs/PV.java" in
+  generate_java_struct "PV" pv_cols;
+  close ();
+
+  let close = output_to "java/com/redhat/et/libguestfs/VG.java" in
+  generate_java_struct "VG" vg_cols;
+  close ();
+
+  let close = output_to "java/com/redhat/et/libguestfs/LV.java" in
+  generate_java_struct "LV" lv_cols;
+  close ();
+
+  let close = output_to "java/com/redhat/et/libguestfs/Stat.java" in
+  generate_java_struct "Stat" stat_cols;
+  close ();
+
+  let close = output_to "java/com/redhat/et/libguestfs/StatVFS.java" in
+  generate_java_struct "StatVFS" statvfs_cols;
+  close ();
+
+  let close = output_to "java/com_redhat_et_libguestfs_GuestFS.c" in
+  generate_java_c ();
+  close ();
+
+  let close = output_to "java/Bindtests.java" in
+  generate_java_bindtests ();
+  close ();
+
+  let close = output_to "haskell/Guestfs.hs" in
+  generate_haskell_hs ();
+  close ();
+
+  let close = output_to "haskell/Bindtests.hs" in
+  generate_haskell_bindtests ();
+  close ();
+
+  let close = output_to "src/MAX_PROC_NR" in
+  generate_max_proc_nr ();
+  close ();