Page MenuHomePhabricator

Efl.Io Interfaces
Updated 1,119 Days AgoPublic

Introduction

With Eo infrastructure the following interfaces were created with specific purpose to allow uniform low-level access to input and output objects:

  • Efl.Io.Reader reports when an object can be read, if it's end-of-stream and how to read data.
  • Efl.Io.Writer reports when an object can be written to and how to write data to it.
  • Èfl.Io.Closer reports if an object is closed and how to close it.
  • Efl.Io.Sizer reports if an object changed its size, what's the size and how to resize it.
  • Efl.Io.Positioner reports if an object changed its position, what's the position and how to seek it.

Looking at the interfaces show they are very low-level and synchronous, modeled after the C library (libC) functions: read(2), write(2), close(2), ftruncate(2)/fstat(2) and lseek(2), with events that mostly about listening to file descriptors with select(2)/poll(2) such as Efl.Io.Reader.can_read matches when Efl.Loop.Fd reports read events.

The functions are synchronous and there is no buffering implied, however it can be used if desired or needed by the underlying system. They can report errors immediately such as EAGAIN to let users know no data was taken and it should be retried -- same pattern as as libC with O_NONBLOCK.

Classes may declare they support just a subset of these interfaces, such as Efl.Io.Buffer implements all of them, while Efl.Io.Stdin implements just Efl.Io.Reader, while Efl.Io.Stdout implements only Efl.Io.Writer.

User's Point of View

From the point of view of end-users, dealing with such low level APIs is a pain, as it's with libC and end users are not expected to use the low level interfaces directly. It is recommended to use the Efl.Io.Copier instead.

The Efl.Io.Copier takes data from a source object set with source property (ie: efl_io_copier_source_set()). It must implement the Efl.Io.Reader interface and will be monitored with can_read,changed event and the Efl.Io.Reader.can_read property, calling Efl.Io.Reader.read() with a pre-defined chunk size (Efl.Io.Copier.read_chunk_size), storing the read data in an internal buffer (limited up to Efl.Io.Copier.buffer_limit).

The copier then uses a destination object set with destination property (ie: efl_io_copier_destination_set()). It must implement the Efl.Io.Writer interface and will be monitored with can_write,changed event and the Efl.Io.Writer.can_write property, calling Efl.Io.Write.write() with the internal buffer. If a line delimiter was set with Efl.Io.Copier.line_delimiter, then the writes are based on each line (including delimiter), otherwise it will be called with the whole internal buffer.

Users may skip the use of a destination object and use the events data and line to get directly. They can even steal the internal buffer with Efl.Io.Copier.binbuf_steal(), taking ownership of the internal buffer (which will be recreated by the copier if needed).

The copy process stop once Efl.Io.Reader.eos property change to true, notified by eos event. If there is a destination object, then it will also wait until all data is pushed to that object. By that time, the Efl.Io.Copier event done is dispatched.

If you want a dynamic behavior and something to handle the buffering for you, consider using Efl.Io.Queue. It may be used as both source or destination and you can use an unlimited buffer if you wish. This allows for the ease to use "write and forget", as well as using efl_io_queue_slice_get() to peek at the contents without consuming its data.

If the copier is closed with Efl.Io.Closer.close(), then source and destination are also closed if they implement such interface and are still not closed.

If the source and destination implement the Efl.Io.Sizer interface, then the copier will monitor Efl.Io.Sizer.size property with size,changed event from source and apply it on destination. This allows destination to know target size and do required allocations or reporting (such as HTTP client can report the upload Content-Length: ).

NOTE: Efl.Io.Copier is a subclass of Efl.Loop_User, thus it must have a parent that is a loop provider, such as the loop itself as returned by ecore_main_loop_get().

Check the example efl_io_copier_example on how to use the Efl.Io.Copier with a number of different I/O objects such as files, standard input/output/error as well network clients. Other examples also make use of the object and may be used as further reference efl_net_server_example and efl_net_dialer_http_example. A simple example would be:

efl_io_copier_simple_example.c
#define EFL_BETA_API_SUPPORT 1
#define EFL_EO_API_SUPPORT 1
#include <Ecore.h>

static int retval = EXIT_SUCCESS;

static void
_copier_done(void *data, const Eo_Event *event)
{
   fprintf(stderr, "INFO: done\n");
   ecore_main_loop_quit();
}

static void
_copier_error(void *data, const Eo_Event *event)
{
   const Eina_Error *perr = event->info;
   fprintf(stderr, "INFO: error: #%d '%s'\n", *perr, eina_error_msg_get(*perr));
   ecore_main_loop_quit();
   retval = EXIT_FAILURE;
}

EFL_CALLBACKS_ARRAY_DEFINE(copier_cbs,
                           { EFL_IO_COPIER_EVENT_DONE, _copier_done },
                           { EFL_IO_COPIER_EVENT_ERROR, _copier_error });

int main(int argc, char *argv[])
{
   Eo *input, *output, *copier, *loop;

   ecore_init();

   loop = ecore_main_loop_get();
   input = efl_add(EFL_IO_STDIN_CLASS, loop);
   output = efl_add(EFL_IO_STDOUT_CLASS, loop);
   copier = efl_add(EFL_IO_COPIER_CLASS, loop,
                    efl_io_copier_source_set(efl_self, input), /* mandatory */
                    efl_io_copier_destination_set(efl_self, output), /* optional */
                    efl_event_callback_array_add(efl_self, copier_cbs(), NULL) /* recommended, at least EFL_IO_COPIER_EVENT_DONE. */
                    );

   ecore_main_loop_begin();

   efl_io_closer_close(copier);
   efl_del(copier);
   efl_del(output);
   efl_del(input);

   ecore_shutdown();
   return retval;
}

// compile: gcc -o example example.c `pkg-config --libs --cflags ecore`
// run: ./example
// quit: ^D (closes stdin) or ^C (quits main loop)

Developer's Point of View

Whenever implementing these interfaces for your class, keep in mind the expected behavior:

  • try to avoid buffering, it's done elsewhere (ie: Efl.Io.Copier);
  • always report events after the internal value is set. If reporting Efl.Io.Reader.can_read changed with can_read,changed, then first set the internal value that the can_read returns, then dispatch the events.
  • remember to set the properties to false if an operation would block or fail. This is essential to the operation of users.

Easy scenario: File Descriptors

To aid the task for the common file-descriptor based classes we provide the following mixins:

  • Efl.Io.Reader.Fd will use read(2) on the file descriptor set with Efl.Io.Reader.Fd.reader_fd property. No monitoring will happen, do it an call Efl.Io.Reader.can_read.set() and Efl.Io.Reader.eos.set() yourself, state is kept and events will be dispatched automatically.
  • Efl.Io.Writer.Fd will use write(2) on the file descriptor set with Efl.Io.Writer.Fd.writer_fd property. No monitoring will happen, do it an call Efl.Io.Writer.can_write.set() yourself, state is kept and events will be dispatched automatically.
  • Efl.Io.Closer.Fd will use close(2) on the file descriptor set with Efl.Io.Closer.Fd.closer_fd property.
  • Efl.Io.Sizer.Fd will use ftruncate(2) and fstat(2) on the file descriptor set with Efl.Io.Sizer.Fd.sizer_fd property.
  • Efl.Io.Positioner.Fd will use lseek(2) on the file descriptor set with Efl.Io.Positioner.positioner_fd property.

The monitoring can be done with Efl.Loop.Fd, as is the case with Efl.Io.Stdin, Efl.Io.Stdout and
Efl.Io.Stderr.

Complex Scenario: Efl.Net.Dialer.Http (cURL)

Sometimes the object is on top of more complex objects or libraries with a different work model. This is the case with Efl.Net.Dialer.Http which uses cURL to implement a HTTP client. These cases need more careful analysis and implementation that reduces the overhead, cooperating with the required interfaces.

For Efl.Net.Dialer.Http, cURL will do the monitoring of the filedescriptor and call you to send data ( CURLOPT_READFUNC reads data into cURL, to be sent to server) or receive data (CURLOPT_WRITEFUNC reports data cURL received, to be written to your application). These are immediate calls and thus the interfaces were implemented as following:

  • Efl.Io.Writer.write()=_efl_net_dialer_http_efl_io_writer_write() will set an internal pointer (pd->send.slice) to the provided buffer to be written then it will trigger cURL to operate on the filedescriptor (curl_multi_socket_action()), which will cause CURLOPT_READFUNC=_efl_net_dialer_http_send_data() to be called. That function will adjust the internal pointer to state how much was used by cURL and return. Then Efl.Io.Writer.write() will adjust its own return pointers and value based on that. No double buffering is needed.
  • Efl.Io.Reader.read()=_efl_net_dialer_http_efl_io_reader_read() will use an internal buffer that is appended from CURLOPT_WRITEFUNC=_efl_net_dialer_http_receive_data() up to a limit (pd->recv.limit). Then Efl.Io.Reader.read() will consume from that buffer, enabling more data to be fed to it by cURL.

The can_read and can_write are adjusted to control the flow coming to/from the external peers (ie: Efl.Io.Copier). In order to control cURL's own flow, pd->pause with`curl_easy_pause()` and return values are used. These will provide bi-directional control over flow, not allowing the memory to blow.

Last Author
barbieri
Last Edited
Aug 23 2016, 4:27 PM
Projects
None
Subscribers
None