Filesystem¶
Simple filesystem read/write is achieved using the uv_fs_*
functions and the
uv_fs_t
struct.
Note
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 application interaction is required.
All filesystem functions have two forms - synchronous and asynchronous.
The synchronous forms automatically get called (and block) if the callback is null. The return value of functions is a libuv error code. This is usually only useful for synchronous calls. The asynchronous form is called when a callback is passed and the return value is 0.
Reading/Writing files¶
A file descriptor is obtained using
intuv_fs_open(uv_loop_t*loop,uv_fs_t*req,constchar*path,intflags,intmode,uv_fs_cbcb)
flags
and mode
are standard
Unix flags.
libuv takes care of converting to the appropriate Windows flags.
File descriptors are closed using
intuv_fs_close(uv_loop_t*loop,uv_fs_t*req,uv_filefile,uv_fs_cbcb)
Filesystem operation callbacks have the signature:
voidcallback(uv_fs_t*req);
Let’s see a simple implementation of cat
. We start with registering
a callback for when the file is opened:
uvcat/main.c - opening a file
1// The request passed to the callback is the same as the one the call setup 2// function was passed. 3assert(req==&open_req); 4if(req->result>=0){ 5iov=uv_buf_init(buffer,sizeof(buffer)); 6uv_fs_read(uv_default_loop(),&read_req,req->result, 7&iov,1,-1,on_read); 8} 9else{ 10fprintf(stderr,"error opening file: %s\n",uv_strerror((int)req->result)); 11} 12} 13
The result
field of a uv_fs_t
is the file descriptor in case of the
uv_fs_open
callback. If the file is successfully opened, we start reading it.
uvcat/main.c - read callback
1if(req->result<0){ 2fprintf(stderr,"Read error: %s\n",uv_strerror(req->result)); 3} 4elseif(req->result==0){ 5uv_fs_tclose_req; 6// synchronous 7uv_fs_close(uv_default_loop(),&close_req,open_req.result,NULL); 8} 9elseif(req->result>0){ 10iov.len=req->result; 11uv_fs_write(uv_default_loop(),&write_req,1,&iov,1,-1,on_write); 12} 13} 14
In the case of a read call, you should pass an initialized buffer which will
be filled with data before the read callback is triggered. The uv_fs_*
operations map almost directly to certain POSIX functions, so EOF is indicated
in this case by result
being 0. In the case of streams or pipes, the
UV_EOF
constant would have been passed as a status instead.
Here you see a common pattern when writing asynchronous programs. The
uv_fs_close()
call is performed synchronously. Usually tasks which are
one-off, or are done as part of the startup or shutdown stage are performed
synchronously, since we are interested in fast I/O when the program is going
about its primary task and dealing with multiple I/O sources. For solo tasks
the performance difference usually is negligible and may lead to simpler code.
Filesystem writing is similarly simple using uv_fs_write()
. Your callback
will be triggered after the write is complete. In our case the callback
simply drives the next read. Thus read and write proceed in lockstep via
callbacks.
uvcat/main.c - write callback
1if(req->result<0){ 2fprintf(stderr,"Write error: %s\n",uv_strerror((int)req->result)); 3} 4else{ 5uv_fs_read(uv_default_loop(),&read_req,open_req.result,&iov,1,-1,on_read); 6} 7} 8
Warning
Due to the way filesystems and disk drives are configured for performance, a write that ‘succeeds’ may not be committed to disk yet.
We set the dominos rolling in main()
:
uvcat/main.c
1uv_fs_open(uv_default_loop(),&open_req,argv[1],O_RDONLY,0,on_open); 2uv_run(uv_default_loop(),UV_RUN_DEFAULT); 3 4uv_fs_req_cleanup(&open_req); 5uv_fs_req_cleanup(&read_req); 6uv_fs_req_cleanup(&write_req); 7return0; 8}
Warning
The uv_fs_req_cleanup()
function must always be called on filesystem
requests to free internal memory allocations in libuv.
Filesystem operations¶
All the standard filesystem operations like unlink
, rmdir
, stat
are
supported asynchronously and have intuitive argument order. They follow the
same patterns as the read/write/open calls, returning the result in the
uv_fs_t.result
field. The full list:
Filesystem operations
intuv_fs_close(uv_loop_t*loop,uv_fs_t*req,uv_filefile,uv_fs_cbcb); intuv_fs_open(uv_loop_t*loop,uv_fs_t*req,constchar*path,intflags,intmode,uv_fs_cbcb); intuv_fs_read(uv_loop_t*loop,uv_fs_t*req,uv_filefile,constuv_buf_tbufs[],unsignedintnbufs,int64_toffset,uv_fs_cbcb); intuv_fs_unlink(uv_loop_t*loop,uv_fs_t*req,constchar*path,uv_fs_cbcb); intuv_fs_write(uv_loop_t*loop,uv_fs_t*req,uv_filefile,constuv_buf_tbufs[],unsignedintnbufs,int64_toffset,uv_fs_cbcb); intuv_fs_copyfile(uv_loop_t*loop,uv_fs_t*req,constchar*path,constchar*new_path,intflags,uv_fs_cbcb); intuv_fs_mkdir(uv_loop_t*loop,uv_fs_t*req,constchar*path,intmode,uv_fs_cbcb); intuv_fs_mkdtemp(uv_loop_t*loop,uv_fs_t*req,constchar*tpl,uv_fs_cbcb); intuv_fs_mkstemp(uv_loop_t*loop,uv_fs_t*req,constchar*tpl,uv_fs_cbcb); intuv_fs_rmdir(uv_loop_t*loop,uv_fs_t*req,constchar*path,uv_fs_cbcb); intuv_fs_scandir(uv_loop_t*loop,uv_fs_t*req,constchar*path,intflags,uv_fs_cbcb); intuv_fs_scandir_next(uv_fs_t*req,uv_dirent_t*ent); intuv_fs_opendir(uv_loop_t*loop,uv_fs_t*req,constchar*path,uv_fs_cbcb); intuv_fs_readdir(uv_loop_t*loop,uv_fs_t*req,uv_dir_t*dir,uv_fs_cbcb); intuv_fs_closedir(uv_loop_t*loop,uv_fs_t*req,uv_dir_t*dir,uv_fs_cbcb); intuv_fs_stat(uv_loop_t*loop,uv_fs_t*req,constchar*path,uv_fs_cbcb); intuv_fs_fstat(uv_loop_t*loop,uv_fs_t*req,uv_filefile,uv_fs_cbcb); intuv_fs_rename(uv_loop_t*loop,uv_fs_t*req,constchar*path,constchar*new_path,uv_fs_cbcb); intuv_fs_fsync(uv_loop_t*loop,uv_fs_t*req,uv_filefile,uv_fs_cbcb); intuv_fs_fdatasync(uv_loop_t*loop,uv_fs_t*req,uv_filefile,uv_fs_cbcb); intuv_fs_ftruncate(uv_loop_t*loop,uv_fs_t*req,uv_filefile,int64_toffset,uv_fs_cbcb); intuv_fs_sendfile(uv_loop_t*loop,uv_fs_t*req,uv_fileout_fd,uv_filein_fd,int64_tin_offset,size_tlength,uv_fs_cbcb); intuv_fs_access(uv_loop_t*loop,uv_fs_t*req,constchar*path,intmode,uv_fs_cbcb); intuv_fs_chmod(uv_loop_t*loop,uv_fs_t*req,constchar*path,intmode,uv_fs_cbcb); intuv_fs_utime(uv_loop_t*loop,uv_fs_t*req,constchar*path,doubleatime,doublemtime,uv_fs_cbcb); intuv_fs_futime(uv_loop_t*loop,uv_fs_t*req,uv_filefile,doubleatime,doublemtime,uv_fs_cbcb); intuv_fs_lutime(uv_loop_t*loop,uv_fs_t*req,constchar*path,doubleatime,doublemtime,uv_fs_cbcb); intuv_fs_lstat(uv_loop_t*loop,uv_fs_t*req,constchar*path,uv_fs_cbcb); intuv_fs_link(uv_loop_t*loop,uv_fs_t*req,constchar*path,constchar*new_path,uv_fs_cbcb); intuv_fs_symlink(uv_loop_t*loop,uv_fs_t*req,constchar*path,constchar*new_path,intflags,uv_fs_cbcb); intuv_fs_readlink(uv_loop_t*loop,uv_fs_t*req,constchar*path,uv_fs_cbcb); intuv_fs_realpath(uv_loop_t*loop,uv_fs_t*req,constchar*path,uv_fs_cbcb); intuv_fs_fchmod(uv_loop_t*loop,uv_fs_t*req,uv_filefile,intmode,uv_fs_cbcb); intuv_fs_chown(uv_loop_t*loop,uv_fs_t*req,constchar*path,uv_uid_tuid,uv_gid_tgid,uv_fs_cbcb); intuv_fs_fchown(uv_loop_t*loop,uv_fs_t*req,uv_filefile,uv_uid_tuid,uv_gid_tgid,uv_fs_cbcb); intuv_fs_lchown(uv_loop_t*loop,uv_fs_t*req,constchar*path,uv_uid_tuid,uv_gid_tgid,uv_fs_cbcb); intuv_fs_statfs(uv_loop_t*loop,uv_fs_t*req,constchar*path,uv_fs_cbcb);
Buffers and Streams¶
The basic I/O handle in libuv is the stream (uv_stream_t
). TCP sockets, UDP
sockets, and pipes for file I/O and IPC are all treated as stream subclasses.
Streams are initialized using custom functions for each subclass, then operated upon using
intuv_read_start(uv_stream_t*,uv_alloc_cballoc_cb,uv_read_cbread_cb); intuv_read_stop(uv_stream_t*); intuv_write(uv_write_t*req,uv_stream_t*handle, constuv_buf_tbufs[],unsignedintnbufs,uv_write_cbcb);
The stream based functions are simpler to use than the filesystem ones and
libuv will automatically keep reading from a stream when uv_read_start()
is
called once, until uv_read_stop()
is called.
The discrete unit of data is the buffer – uv_buf_t
. This is simply
a collection of a pointer to bytes (uv_buf_t.base
) and the length
(uv_buf_t.len
). The uv_buf_t
is lightweight and passed around by value.
What does require management is the actual bytes, which have to be allocated
and freed by the application.
Error
THIS PROGRAM DOES NOT ALWAYS WORK, NEED SOMETHING BETTER
To demonstrate streams we will need to use uv_pipe_t
. This allows streaming
local files [2]. Here is a simple tee utility using libuv. Doing all operations
asynchronously shows the power of evented I/O. The two writes won’t block each
other, but we have to be careful to copy over the buffer data to ensure we don’t
free a buffer until it has been written.
The program is to be executed as:
./uvtee <output_file>
We start off opening pipes on the files we require. libuv pipes to a file are opened as bidirectional by default.
uvtee/main.c - read on pipes
1loop=uv_default_loop(); 2 3uv_pipe_init(loop,&stdin_pipe,0); 4uv_pipe_open(&stdin_pipe,0); 5 6uv_pipe_init(loop,&stdout_pipe,0); 7uv_pipe_open(&stdout_pipe,1); 8 9uv_fs_tfile_req; 10intfd=uv_fs_open(loop,&file_req,argv[1],O_CREAT|O_RDWR,0644,NULL); 11uv_pipe_init(loop,&file_pipe,0); 12uv_pipe_open(&file_pipe,fd); 13 14uv_read_start((uv_stream_t*)&stdin_pipe,alloc_buffer,read_stdin); 15 16uv_run(loop,UV_RUN_DEFAULT); 17return0; 18}
The third argument of uv_pipe_init()
should be set to 1 for IPC using named
pipes. This is covered in Processes. The uv_pipe_open()
call
associates the pipe with the file descriptor, in this case 0
(standard
input).
We start monitoring stdin
. The alloc_buffer
callback is invoked as new
buffers are required to hold incoming data. read_stdin
will be called with
these buffers.
uvtee/main.c - reading buffers
1*buf=uv_buf_init((char*)malloc(suggested_size),suggested_size); 2} 3 4voidfree_write_req(uv_write_t*req){ 5if(nread<0){ 6if(nread==UV_EOF){ 7// end of file 8uv_close((uv_handle_t*)&stdin_pipe,NULL); 9uv_close((uv_handle_t*)&stdout_pipe,NULL); 10uv_close((uv_handle_t*)&file_pipe,NULL); 11} 12}elseif(nread>0){ 13write_data((uv_stream_t*)&stdout_pipe,nread,*buf,on_stdout_write); 14write_data((uv_stream_t*)&file_pipe,nread,*buf,on_file_write); 15} 16 17// OK to free buffer as write_data copies it. 18if(buf->base) 19free(buf->base); 20} 21
The standard malloc
is sufficient here, but you can use any memory allocation
scheme. For example, node.js uses its own slab allocator which associates
buffers with V8 objects.
The read callback nread
parameter is less than 0 on any error. This error
might be EOF, in which case we close all the streams, using the generic close
function uv_close()
which deals with the handle based on its internal type.
Otherwise nread
is a non-negative number and we can attempt to write that
many bytes to the output streams. Finally remember that buffer allocation and
deallocation is application responsibility, so we free the data.
The allocation callback may return a buffer with length zero if it fails to
allocate memory. In this case, the read callback is invoked with error
UV_ENOBUFS. libuv will continue to attempt to read the stream though, so you
must explicitly call uv_close()
if you want to stop when allocation fails.
The read callback may be called with nread = 0
, indicating that at this
point there is nothing to be read. Most applications will just ignore this.
uvtee/main.c - Write to pipe
1uv_write_treq; 2uv_buf_tbuf; 3}write_req_t; 4 5uv_loop_t*loop; 6write_req_t*wr=(write_req_t*)req; 7free(wr->buf.base); 8free(wr); 9} 10 11voidon_stdout_write(uv_write_t*req,intstatus){ 12free_write_req(req); 13} 14 15voidon_file_write(uv_write_t*req,intstatus){ 16free_write_req(req); 17} 18 19voidwrite_data(uv_stream_t*dest,size_tsize,uv_buf_tbuf,uv_write_cbcb){ 20write_req_t*req=(write_req_t*)malloc(sizeof(write_req_t)); 21req->buf=uv_buf_init((char*)malloc(size),size); 22memcpy(req->buf.base,buf.base,size); 23uv_write((uv_write_t*)req,(uv_stream_t*)dest,&req->buf,1,cb); 24} 25
write_data()
makes a copy of the buffer obtained from read. This buffer
does not get passed through to the write callback trigged on write completion. To
get around this we wrap a write request and a buffer in write_req_t
and
unwrap it in the callbacks. We make a copy so we can free the two buffers from
the two calls to write_data
independently of each other. While acceptable
for a demo program like this, you’ll probably want smarter memory management,
like reference counted buffers or a pool of buffers in any major application.
Warning
If your program is meant to be used with other programs it may knowingly or unknowingly be writing to a pipe. This makes it susceptible to aborting on receiving a SIGPIPE. It is a good idea to insert:
signal(SIGPIPE, SIG_IGN)
in the initialization stages of your application.
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]. This is one of the more inconsistent parts of libuv. File change notification systems are themselves extremely varied across platforms so getting everything working everywhere is difficult. To demonstrate, I’m going to build a simple utility which runs a command whenever any of the watched files change:
./onchange <command> <file1> [file2] ...
Note
Currently this example only works on OSX and Windows. Refer to the notes of uv_fs_event_start function.
The file change notification is started using uv_fs_event_init()
:
onchange/main.c - The setup
1intmain(intargc,char**argv){ 2if(argc<=2){ 3fprintf(stderr,"Usage: %s <command> <file1> [file2 ...]\n",argv[0]); 4return1; 5} 6 7loop=uv_default_loop(); 8command=argv[1]; 9 10while(argc-->2){ 11fprintf(stderr,"Adding watch on %s\n",argv[argc]); 12uv_fs_event_t*fs_event_req=malloc(sizeof(uv_fs_event_t)); 13uv_fs_event_init(loop,fs_event_req); 14// The recursive flag watches subdirectories too. 15uv_fs_event_start(fs_event_req,run_command,argv[argc],UV_FS_EVENT_RECURSIVE); 16} 17 18returnuv_run(loop,UV_RUN_DEFAULT); 19}
The third argument is the actual file or directory to monitor. The last
argument, flags
, can be:
/* * Flags to be passed to uv_fs_event_start(). */ enumuv_fs_event_flags{ UV_FS_EVENT_WATCH_ENTRY=1, UV_FS_EVENT_STAT=2, UV_FS_EVENT_RECURSIVE=4 };
UV_FS_EVENT_WATCH_ENTRY
and UV_FS_EVENT_STAT
don’t do anything (yet).
UV_FS_EVENT_RECURSIVE
will start watching subdirectories as well on
supported platforms.
The callback will receive the following arguments:
uv_fs_event_t *handle
- The handle. Thepath
field of the handle is the file on which the watch was set.
const char *filename
- If a directory is being monitored, this is the file which was changed. Only non-null
on Linux and Windows. May benull
even on those platforms.
int events
- one ofUV_RENAME
orUV_CHANGE
, or a bitwise OR of both.
int status
- Ifstatus < 0
, there is an libuv error.
In our example we simply print the arguments and run the command using
system()
.
onchange/main.c - file change notification callback
1voidrun_command(uv_fs_event_t*handle,constchar*filename,intevents,intstatus){ 2charpath[1024]; 3size_tsize=1023; 4// Does not handle error if path is longer than 1023. 5uv_fs_event_getpath(handle,path,&size); 6path[size]='0円'; 7 8fprintf(stderr,"Change detected in %s: ",path); 9if(events&UV_RENAME) 10fprintf(stderr,"renamed"); 11if(events&UV_CHANGE) 12fprintf(stderr,"changed"); 13 14fprintf(stderr," %s\n",filename?filename:""); 15system(command); 16}