The libuv filesystem operations are different from socket operations. Socket operations use the non-blocking operations provided by the operating system. Filesystem operations use blocking functions internally, but invoke these functions in a thread pool and notify watchers registered with the event loop when they complete.

All filesystem operations have two forms — synchronous and asynchronous.

Synchronous operations are slightly easier to program with, and are faster, because they run in the current thread instead of the thread pool (note again: this is only an issue for filesystem operations, not sockets or pipes). However, they can block the thread. This can happen, for example, if you use a synchronous operation to access a network filesystem which is slow or unavailable. It can also happen locally. So, synchronous operations should not be used in robust applications or libraries. Synchronous filesystem operations are found in module Luv.File.Sync.

Asynchronous versions of all the same operations are found in the main module Luv.File. These each take a callback, to which they pass their result, instead of returning their result directly, as synchronous operations do.

As already mentioned, asynchronous filesystem operations are slower than synchronous, because running an asynchronous operation requires communicating twice with a thread in the libuv thread pool: once to start the operation, and once more when it is complete. In some cases, you can mitigate this cost by manually running multiple operations in one request in the thread pool. To do that, use Luv.Thread_pool.queue_work, and run multiple synchronous operations in the function you pass to it.

Note that this only offers a performance benefit if you run multiple operations. Running only one operation is equivalent to simply using the asynchronous API — that’s how the asynchronous API is implemented.

The rest of this chapter sticks to the asynchronous API.

Reading/writing files#

A file descriptor is obtained by calling Luv.File.open_:

Luv.File.open_ :
  ?mode:Luv.File.Mode.t list ->
  string ->
  Luv.File.Open_flag.t list ->
  ((Luv.File.t, Luv.Error.t) result -> unit) ->

Despite the verbose signature, usage is simple:

Luv.File.open_ "foo" [`RDONLY] (fun result -> (* ... *))

Luv.File.Open_flag.t and Luv.File.Mode.t expose the standard Unix open flags and file modes, respectively. libuv takes care of converting them to appropriate values on Windows. mode is optional, because it is only used if open_ creates the file.

File descriptors are closed using Luv.File.close:

Luv.File.close : Luv.File.t -> ((unit, Luv.Error.t) result -> unit) -> unit

Finally, reading and writing are done with and Luv.File.write: :
  Luv.File.t ->
  Luv.Buffer.t list ->
  ((Unsigned.Size_t.t, Luv.Error.t) result -> unit) ->

Luv.File.write :
  Luv.File.t ->
  Luv.Buffer.t list ->
  ((Unsigned.Size_t.t, Luv.Error.t) result -> unit) ->

Let’s put all this together into a simple implementation of cat:

 1let () =
 2  Luv.File.open_ Sys.argv.(1) [`RDONLY] begin function
 3    | Error e ->
 4      Printf.eprintf "Error opening file: %s\n" (Luv.Error.strerror e)
 5    | Ok file ->
 6      let buffer = Luv.Buffer.create 1024 in
 8      let rec on_read = function
 9        | Error e ->
10          Printf.eprintf "Read error: %s\n" (Luv.Error.strerror e)
11        | Ok bytes_read ->
12          let bytes_read = Unsigned.Size_t.to_int bytes_read in
13          if bytes_read = 0 then
14            Luv.File.close file ignore
15          else
16            Luv.File.write
17              Luv.File.stdout
18              [Luv.Buffer.sub buffer ~offset:0 ~length:bytes_read]
19              on_write
21      and on_write = function
22        | Error e ->
23          Printf.eprintf "Write error: %s\n" (Luv.Error.strerror e)
24        | Ok _bytes_written ->
25 file [buffer] on_read
27      in
29 file [buffer] on_read
30  end;
32  ignore ( () : bool)


on_write is not robust, because we don’t check that the number of bytes written is actually the number of bytes we requested to write. Fewer bytes could have been written, in which case we would need to try again to write the remaining bytes.


Due to the way filesystems and disk drives are configured for performance, a write that succeeds may not be committed to disk yet.

Filesystem operations#

All the standard filesystem operations like unlink, rmdir, stat are supported both synchronously and asynchronously. The easiest way to see the full list, with simplified signatures, is to scroll down through Luv.File.Sync.

File change events#

All modern operating systems provide APIs to put watches on individual files or directories and be informed when the files are modified. libuv wraps common file change notification libraries [1], and the wrapper is exposed by Luv as module Luv.FS_event. To demonstrate, let’s build a simple utility which runs a command whenever any of the watched files change:

./onchange <command> <file1> [file2] ...

 1let () =
 2  match Array.to_list Sys.argv with
 3  | [] | [_] | [_; _] ->
 4    Printf.eprintf "Usage: onchange <command> <file1> [file2]..."
 6  | _::command::files ->
 7    files |> List.iter begin fun target ->
 8      let watcher = Luv.FS_event.init () |> Result.get_ok in
10      Luv.FS_event.start ~recursive:true watcher target begin function
11        | Error e ->
12          Printf.eprintf
13            "Error watching %s: %s\n" target (Luv.Error.strerror e);
14          ignore (Luv.FS_event.stop watcher);
15          Luv.Handle.close watcher ignore
17        | Ok (file, events) ->
18          if List.mem `RENAME events then
19            prerr_string "renamed ";
20          if List.mem `CHANGE events then
21            prerr_string "changed ";
22          prerr_endline file;
24          let exit_status = Sys.command command in
25          if exit_status <> 0 then
26            Stdlib.exit exit_status
27      end
28    end;
30  ignore ( () : bool)