driver_entry(3)

πŸ“¦
version 29.0.6 Β· 3 versions
, in ubuntu stonking

πŸ’– driver_entry β€” the driver entry struct πŸ’–

ok so this is driver_entry, from the Erlang manual page driver_entry(3) (yes bestie, it's a C module definition page, we're doing C today, deep breaths 🌸). It's the driver-entry structure used by Erlang drivers. Periodt.


🚨 DESCRIPTION 🚨

WARNING: Use this functionality with extreme care. A driver callback is executed as a direct extension of the native code of the VM. Execution is not made in a safe environment. The VM cannot provide the same services as provided when executing Erlang code, such as pre-emptive scheduling or memory protection. If the driver callback function does not behave well, the whole VM will misbehave.

ok so like. no pressure or anything πŸ’€

  • a driver callback that crashes will crash the whole VM πŸ’€πŸ’€
  • an erroneously implemented driver callback can cause a VM internal state inconsistency, which can cause a crash of the VM, or miscellaneous misbehaviors of the VM at any point after the call to the driver callback πŸ™ˆ
  • a driver callback doing lengthy work before returning degrades responsiveness of the VM, and can cause miscellaneous strange behaviors. Such strange behaviors include, but are not limited to, extreme memory usage, and bad load balancing between schedulers. Strange behaviors that can occur because of lengthy work can also vary between Erlang/OTP releases ✨

As from ERTS 5.9 (Erlang/OTP R15B) the driver interface has been changed with larger types for the callbacks output, control, and call. See driver version management in erl_driver.

NOTE: Old drivers (compiled with an erl_driver.h from an ERTS version earlier than 5.9) must be updated and have to use the extended interface (with version management). fr fr, no skipping this one.

Okay so the driver_entry structure is a C struct that all Erlang drivers define. It contains entry points for the Erlang driver, which are called by the Erlang emulator when Erlang code accesses the driver. Main character energy honestly πŸ‘‘

The erl_driver driver API functions need a port handle that identifies the driver instance (and the port in the emulator). This is only passed to the start function, but not to the other functions. The start function returns a driver-defined handle that is passed to the other functions. A common practice is to have the start function allocate some application-defined structure and stash the port handle in it, to use it later with the driver API functions. iykyk 🫢

The driver callback functions are called synchronously from the Erlang emulator. If they take too long before completing, they can cause time-outs in the emulator. Use the queue or asynchronous calls if necessary, as the emulator must be responsive. (the emulator is a busy queen, she does not wait for you πŸ’…)

The driver structure contains the driver name and some 15 function pointers, which are called at different times by the emulator.

The only exported function from the driver is driver_init. This function returns the driver_entry structure that points to the other functions in the driver. The driver_init function is declared with a macro, DRIVER_INIT(drivername). (This is because different operating systems have different names for it. classic.)

When writing a driver in C++, the driver entry is to be of "C" linkage. One way to do this is to put the following line somewhere before the driver entry:

extern "C" DRIVER_INIT(drivername);

When the driver has passed the driver_entry over to the emulator, the driver is not allowed to modify the driver_entry. hands off bestie πŸ™…β€β™€οΈ

If compiling a driver for static inclusion through --enable-static-drivers, you must define STATIC_ERLANG_DRIVER before the DRIVER_INIT declaration.

NOTE: Do not declare the driver_entry const. This because the emulator must modify the handle and the handle2 fields. A statically allocated, and const-declared driver_entry can be located in read-only memory, which causes the emulator to crash. so like, don't. seriously. 🫠

🌈 Data Types 🌈

πŸ¦‹ ErlDrvEntry

typedef struct erl_drv_entry {
    int (*init)(void);          /* Called at system startup for statically
                                   linked drivers, and after loading for
                                   dynamically loaded drivers */
#ifndef ERL_SYS_DRV
    ErlDrvData (*start)(ErlDrvPort port, char *command);
                                /* Called when open_port/2 is invoked,
                                   return value -1 means failure */
#else
    ErlDrvData (*start)(ErlDrvPort port, char *command, SysDriverOpts* opts);
                                /* Special options, only for system driver */
#endif
    void (*stop)(ErlDrvData drv_data);
                                /* Called when port is closed, and when the
                                   emulator is halted */
    void (*output)(ErlDrvData drv_data, char *buf, ErlDrvSizeT len);
                                /* Called when we have output from Erlang to
                                   the port */
    void (*ready_input)(ErlDrvData drv_data, ErlDrvEvent event);
                                /* Called when we have input from one of
                                   the driver's handles */
    void (*ready_output)(ErlDrvData drv_data, ErlDrvEvent event);
                                /* Called when output is possible to one of
                                   the driver's handles */
    char *driver_name;          /* Name supplied as command in
                                   erlang:open_port/2 */
    void (*finish)(void);       /* Called before unloading the driver -
                                   dynamic drivers only */
    void *handle;               /* Reserved, used by emulator internally */
    ErlDrvSSizeT (*control)(ErlDrvData drv_data, unsigned int command,
                            char *buf, ErlDrvSizeT len,
                   char **rbuf, ErlDrvSizeT rlen);
                                /* "ioctl" for drivers - invoked by
                                   port_control/3 */
    void (*timeout)(ErlDrvData drv_data);
                                /* Handling of time-out in driver */
    void (*outputv)(ErlDrvData drv_data, ErlIOVec *ev);
                                /* Called when we have output from Erlang
                                   to the port */
    void (*ready_async)(ErlDrvData drv_data, ErlDrvThreadData thread_data);
    void (*flush)(ErlDrvData drv_data);
                                /* Called when the port is about to be
                                   closed, and there is data in the
                                   driver queue that must be flushed
                                   before 'stop' can be called */
    ErlDrvSSizeT (*call)(ErlDrvData drv_data, unsigned int command,
                         char *buf, ErlDrvSizeT len,
                char **rbuf, ErlDrvSizeT rlen, unsigned int *flags);
                                /* Works mostly like 'control', a synchronous
                                   call into the driver */
    void* unused_event_callback;
    int extended_marker;        /* ERL_DRV_EXTENDED_MARKER */
    int major_version;          /* ERL_DRV_EXTENDED_MAJOR_VERSION */
    int minor_version;          /* ERL_DRV_EXTENDED_MINOR_VERSION */
    int driver_flags;           /* ERL_DRV_FLAGs */
    void *handle2;              /* Reserved, used by emulator internally */
    void (*process_exit)(ErlDrvData drv_data, ErlDrvMonitor *monitor);
                                /* Called when a process monitor fires */
    void (*stop_select)(ErlDrvEvent event, void* reserved);
                                /* Called to close an event object */
 } ErlDrvEntry;

🌟 int (*init)(void)

Called directly after the driver has been loaded by erl_ddll:load_driver/2 (actually when the driver is added to the driver list). The driver is to return 0, or, if the driver cannot initialize, -1.

like, hello? are you okay? 0 = yes queen, -1 = we're not doing this today βœ‹

🌸 ErlDrvData (*start)(ErlDrvPort port, char* command)

Called when the driver is instantiated, when erlang:open_port/2 is called. The driver is to return a number >= 0 or a pointer, or, if the driver cannot be started, one of three error codes:

  • ERL_DRV_ERROR_GENERAL β€” General error, no error code
  • ERL_DRV_ERROR_ERRNO β€” Error with error code in errno
  • ERL_DRV_ERROR_BADARG β€” Error, badarg

If an error code is returned, the port is not started. no port for you bestie πŸ’…

πŸŽ€ void (*stop)(ErlDrvData drv_data)

Called when the port is closed, with erlang:port_close/1 or Port ! {self(), close}. Notice that terminating the port owner process also closes the port. If drv_data is a pointer to memory allocated in start, then stop is the place to deallocate that memory. this is your cleanup era 🧹✨

🫢 void (*output)(ErlDrvData drv_data, char *buf, ErlDrvSizeT len)

Called when an Erlang process has sent data to the port. The data is pointed to by buf, and is len bytes. Data is sent to the port with Port ! {self(), {command, Data}} or with erlang:port_command/2. Depending on how the port was opened, it is to be either a list of integers 0...255 or a binary. See erlang:open_port/2 and erlang:port_command/2.

  • void (*ready_input)(ErlDrvData drv_data, ErlDrvEvent event)

πŸ’œ void (*ready_output)(ErlDrvData drv_data, ErlDrvEvent event)

Called when a driver event (specified in parameter event) is signaled. This is used to help asynchronous drivers "wake up" when something occurs. ✨ wake up bestie ✨

On Unix the event is a pipe or socket handle (or something that the select system call understands).

On Windows the event is an Event or Semaphore (or something that the WaitForMultipleObjects API function understands). (Some trickery in the emulator allows more than the built-in limit of 64 Events to be used.) slay, loophole πŸ’…

To use this with threads and asynchronous routines, create a pipe on Unix and an Event on Windows. When the routine completes, write to the pipe (use SetEvent on Windows), this makes the emulator call ready_input or ready_output.

False events can occur. That is, calls to ready_input or ready_output although no real events are signaled. In reality, it is rare (and OS-dependant), but a robust driver must nevertheless be able to handle such cases. not the false alarms 😭 be prepared fr

char *driver_name

The driver name. It must correspond to the atom used in erlang:open_port/2, and the name of the driver library file (without the extension).

πŸ¦‹ void (*finish)(void)

Called by the erl_ddll driver when the driver is unloaded. (It is only called in dynamic drivers.)

The driver is only unloaded as a result of calling erl_ddll:unload_driver/1, or when the emulator halts.


ok that's where part 1 leaves us bestie, there's more coming 🌈 stay tuned πŸ’–

  • void *handle β€” bestie, this one's not yours. It's reserved for the emulator's internal use, and the emulator will modify it. Which means: do not declare your driver_entry as const. It has to be mutable or this whole thing goes sideways. πŸ’€
  • ErlDrvSSizeT (*control)(ErlDrvData drv_data, unsigned int command, char *buf, ErlDrvSizeT len, char **rbuf, ErlDrvSizeT rlen) β€” okay so this is the ✨special routine✨ invoked by erlang:port_control/3. Think of it as lowkey an "ioctl" for Erlang drivers, iykyk. The stuff you pass to port_control/3 shows up in buf and len, and your driver can send data back using *rbuf and rlen.

This is the fastest way to call a driver and get a response back β€” no context switch in the Erlang emulator, no message passing, none of that. It's giving "call a C function for faster execution when Erlang is just too slow" 🏎️✨

Want to return data? Put it in rbuf. When control gets called, *rbuf points at a default buffer of rlen bytes that you're free to use for returning data. How the data comes back depends on the port control flags β€” the ones you set with erl_driver:set_port_control_flags.

If the flag is set to PORT_CONTROL_FLAG_BINARY, you return a binary. Small binaries? Just write the raw data into the default buffer, easy. You can also return a binary by setting *rbuf to point at a binary allocated with erl_driver:driver_alloc_binary. That binary gets freed automatically once control returns. If the driver wants to hang on to the binary for read-only access, use erl_driver:driver_binary_inc_refc and free it later with erl_driver:driver_free_binary. But it is never allowed to change the binary after control has returned β€” never ever, no cap. And if *rbuf is set to NULL, you get an empty list back. 🫢

If the flag is set to 0, data comes back as a list of integers. Use the default buffer, or set *rbuf to point at a bigger buffer allocated with erl_driver:driver_alloc. That buffer is freed automatically after control returns.

Ngl, binaries are faster if you're returning more than a few bytes. πŸ’…

The return value is the number of bytes returned in *rbuf.

  • void (*timeout)(ErlDrvData drv_data) β€” called any time after the driver's timer hits 0. You arm the timer with erl_driver:driver_set_timer. There are no priorities or ordering among drivers, so if several drivers time out at the same time? anyone of them can be called first. Chaos, but make it structural. πŸŽ€
  • void (*outputv)(ErlDrvData drv_data, ErlIOVec *ev) β€” called whenever the port gets written to. If it's NULL, the output function gets called instead. This one's faster than output because it takes an ErlIOVec directly, so there's no copying of the data. The port has to be in binary mode, see erlang:open_port/2.

ErlIOVec contains both a SysIOVec (suitable for writev) and one or more binaries. If you want to keep those binaries around after the driver returns from outputv, you can queue them (using, for example, erl_driver:driver_enq_bin), or β€” if you're stashing them in a static or global variable β€” bump the reference counter. 🧚

  • void (*ready_async)(ErlDrvData drv_data, ErlDrvThreadData thread_data) β€” called after an asynchronous call finishes up. You kick that async call off with erl_driver:driver_async. Heads up: this function is called from the Erlang emulator thread, unlike the asynchronous function itself, which gets called in some thread (if multi-threading is enabled). Different thread, different vibe. 🌈
  • void (*flush)(ErlDrvData drv_data) β€” called when the port is about to be closed and there's still data sitting in the driver queue that has to be flushed before 'stop' can be called. 🚰
  • ErlDrvSSizeT (*call)(ErlDrvData drv_data, unsigned int command, char *buf, ErlDrvSizeT len, char **rbuf, ErlDrvSizeT rlen, unsigned int *flags) β€” called from erlang:port_call/3. It works a lot like the control callback, except it uses the external term format for input and output. Glow up fr fr.

command is an integer, obtained from the call from Erlang β€” that's the second argument to erlang:port_call/3.

buf and len give you the arguments to the call (the third argument to erlang:port_call/3). You can decode them using ei functions.

rbuf points to a return buffer, rlen bytes long. The return data has to be a valid Erlang term in the external (binary) format. It gets converted to an Erlang term and handed back by erlang:port_call/3 to whoever called. If you need more space than rlen bytes to return your data, you can set *rbuf to memory allocated with erl_driver:driver_alloc β€” that memory is freed automatically after call returns. 🫧

The return value is the number of bytes returned in *rbuf. And if ERL_DRV_ERROR_GENERAL is returned (or, honestly, anything < 0), erlang:port_call/3 throws a BAD_ARG. Rude but valid. 😭

  • void (*event)(ErlDrvData drv_data, ErlDrvEvent event, ErlDrvEventData event_data) β€” Intentionally left undocumented. Not a typo, not me being lazy, that's just what it says. Mystery field. πŸ™ˆ
  • int extended_marker β€” this field is either equal to ERL_DRV_EXTENDED_MARKER or 0. An old driver that isn't aware of the extended driver interface sets this field to 0. And if this field is 0, all the following fields must also be 0, or NULL if it's a pointer field. No halfsies. ⭐
  • int major_version β€” this field is to equal ERL_DRV_EXTENDED_MAJOR_VERSION if field extended_marker equals ERL_DRV_EXTENDED_MARKER.
  • int minor_version β€” this field is to equal ERL_DRV_EXTENDED_MINOR_VERSION if field extended_marker equals ERL_DRV_EXTENDED_MARKER.
  • int driver_flags β€” used to pass driver capability and other info to the runtime system. If field extended_marker equals ERL_DRV_EXTENDED_MARKER, it's to contain 0 or driver flags (ERL_DRV_FLAG_*) OR'ed bitwise. The following driver flags exist:
    • ERL_DRV_FLAG_USE_PORT_LOCKING β€” the runtime system uses port-level locking on all ports executing this driver instead of driver-level locking. For more information, see erl_driver. πŸ”’
    • ERL_DRV_FLAG_SOFT_BUSY β€” marks that driver instances can handle being called in the output and/or outputv callbacks even though a driver instance has marked itself as busy (see erl_driver:set_busy_port). As from ERTS 5.7.4 this flag is required for drivers used by the Erlang distribution (the behavior has always been required by drivers used by the distribution). So like, not optional, bestie.
    • ERL_DRV_FLAG_NO_BUSY_MSGQ β€” disables busy port message queue functionality. For more information, see erl_driver:erl_drv_busy_msgq_limits. πŸ“­
    • ERL_DRV_FLAG_USE_INIT_ACK β€” when this flag is specified, the linked-in driver must manually acknowledge that the port has been successfully started using erl_driver:erl_drv_init_ack(). This lets the implementor make the erlang:open_port exit with badarg after some initial asynchronous initialization has been done. πŸ‘‘
  • void *handle2 β€” reserved for the emulator's internal use, same deal as handle. The emulator modifies this field, so again: it's important that the driver_entry is not declared const. (This is the part where you go back and check your const. I'll wait. 🫢)
  • void (*process_exit)(ErlDrvData drv_data, ErlDrvMonitor *monitor) β€” called when a monitored process exits. The drv_data is the data associated with the port for which the process is monitored (using erl_driver:driver_monitor_process), and the monitor corresponds to the ErlDrvMonitor structure that got filled in when the monitor was created. The driver interface function erl_driver:driver_get_monitored_process can be used to retrieve the process ID of the exiting process as an ErlDrvTermData. πŸ’œ
  • void (*stop_select)(ErlDrvEvent event, void* reserved) β€” called on behalf of erl_driver:driver_select when it's safe to close an event object. πŸ›‘

A typical implementation on Unix is to do close((int)event).

Argument reserved is intended for future use and is to be ignored. πŸ™ˆ

In contrast to most of the other callback functions, stop_select is called independent of any port. No ErlDrvData argument is passed to the function. No driver lock or port lock is guaranteed to be held. The port that called driver_select can even be closed by the time stop_select is called. But it can also be the case that stop_select is called directly by erl_driver:driver_select.

🚨 Real talk, this one matters: it is not allowed to call any functions in the driver API from stop_select. This strict limitation is because of the volatile context that stop_select can be called in.

🌸 See Also 🌸


and that's a wrap, bestie!! πŸŽ€βœ¨ this lil masterpiece is brought to you by Ericsson AB, running on erts 17.0.6, and yes it's driver_entry(3) β€” go forth and write drivers, you absolute icon πŸ‘‘πŸ’–

πŸ“¦ where this page comes from