查看源代码 driver_entry

Erlang 驱动程序使用的 driver-entry 结构。

描述

警告

请极其小心地使用此功能。

驱动程序回调函数作为 VM 本地代码的直接扩展执行。执行不是在安全的环境中进行的。VM *不能*提供与执行 Erlang 代码时提供的相同服务,例如抢占式调度或内存保护。如果驱动程序回调函数行为不正常,整个 VM 将会行为异常。

  • 发生崩溃的驱动程序回调函数将导致整个 VM 崩溃。
  • 错误实现的驱动程序回调函数可能会导致 VM 内部状态不一致,这可能会导致 VM 崩溃,或者在调用驱动程序回调函数之后的任何时刻发生 VM 的其他各种异常行为。
  • 在返回之前执行长时间工作的驱动程序回调函数会降低 VM 的响应速度,并可能导致各种奇怪的行为。这些奇怪的行为包括但不限于极端的内存使用以及调度器之间的不良负载平衡。由于长时间工作而可能发生的奇怪行为也可能因 Erlang/OTP 版本而异。

从 ERTS 5.9(Erlang/OTP R15B)开始,驱动程序接口已更改,回调函数 outputcontrolcall 的类型更大。请参阅 erl_driver 中的驱动程序版本管理

注意

旧驱动程序(使用早于 5.9 的 ERTS 版本的 erl_driver.h 编译)必须更新并使用扩展接口(带有版本管理)。

driver_entry 结构是一个 C 结构,所有 Erlang 驱动程序都会定义它。它包含 Erlang 驱动程序的入口点,当 Erlang 代码访问驱动程序时,Erlang 模拟器会调用这些入口点。

erl_driver 驱动程序 API 函数需要一个端口句柄,用于标识驱动程序实例(以及模拟器中的端口)。这仅传递给 start 函数,而不传递给其他函数。start 函数返回一个驱动程序定义的句柄,该句柄会传递给其他函数。常见的做法是让 start 函数分配一些应用程序定义的结构并将 port 句柄存储在其中,以便稍后与驱动程序 API 函数一起使用。

驱动程序回调函数从 Erlang 模拟器同步调用。如果它们在完成之前花费的时间太长,它们可能会导致模拟器超时。如有必要,请使用队列或异步调用,因为模拟器必须具有响应能力。

驱动程序结构包含驱动程序名称和大约 15 个函数指针,这些函数指针由模拟器在不同时间调用。

驱动程序中唯一导出的函数是 driver_init。此函数返回指向驱动程序中其他函数的 driver_entry 结构。driver_init 函数使用宏 DRIVER_INIT(drivername) 声明。(这是因为不同的操作系统有不同的名称。)

在 C++ 中编写驱动程序时,驱动程序入口必须具有 "C" 链接。一种方法是在驱动程序入口之前的某个位置添加以下行

extern "C" DRIVER_INIT(drivername);

当驱动程序将 driver_entry 传递给模拟器后,驱动程序*不允许*修改 driver_entry

如果通过 --enable-static-drivers 编译驱动程序以进行静态包含,则必须在 DRIVER_INIT 声明之前定义 STATIC_ERLANG_DRIVER

注意

请*不要*声明 driver_entryconst。这是因为模拟器必须修改 handlehandle2 字段。静态分配的、声明为 constdriver_entry 可能位于只读内存中,这会导致模拟器崩溃。

数据类型

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) - 在驱动程序被 erl_ddll:load_driver/2 加载后(实际上是在将驱动程序添加到驱动程序列表时)直接调用。驱动程序要返回 0;或者,如果驱动程序无法初始化,则返回 -1

  • ErlDrvData (*start)(ErlDrvPort port, char* command) - 在实例化驱动程序(当调用 erlang:open_port/2 时)时调用。驱动程序要返回一个 >= 0 的数字或一个指针;或者,如果驱动程序无法启动,则返回三个错误代码之一

    • ERL_DRV_ERROR_GENERAL - 一般错误,没有错误代码

    • ERL_DRV_ERROR_ERRNO - 错误,错误代码在 errno

    • ERL_DRV_ERROR_BADARG - 错误,badarg

    如果返回错误代码,则端口不会启动。

  • void (*stop)(ErlDrvData drv_data) - 在端口关闭时,通过 erlang:port_close/1Port ! {self(), close} 调用。请注意,终止端口所有者进程也会关闭端口。如果 drv_data 是指向在 start 中分配的内存的指针,则 stop 是释放该内存的地方。

  • void (*output)(ErlDrvData drv_data, char *buf, ErlDrvSizeT len) - 当 Erlang 进程已将数据发送到端口时调用。数据由 buf 指向,长度为 len 字节。使用 Port ! {self(), {command, Data}}erlang:port_command/2 将数据发送到端口。根据端口的打开方式,它要么是整数列表 0...255,要么是二进制。请参阅 erlang:open_port/2erlang:port_command/2

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

  • void (*ready_output)(ErlDrvData drv_data, ErlDrvEvent event) - 当驱动程序事件(在参数 event 中指定)发出信号时调用。这用于帮助异步驱动程序在发生某些事情时“唤醒”。

    在 Unix 上,event 是管道或套接字句柄(或 select 系统调用可以理解的东西)。

    在 Windows 上,eventEventSemaphore(或 WaitForMultipleObjects API 函数可以理解的东西)。(模拟器中的一些技巧允许使用超过内置的 64 个 Event 限制。)

    要将此与线程和异步例程一起使用,请在 Unix 上创建管道,在 Windows 上创建 Event。当例程完成时,写入管道(在 Windows 上使用 SetEvent),这会使模拟器调用 ready_inputready_output

    可能会发生虚假事件。也就是说,尽管没有发出真正的事件信号,也会调用 ready_inputready_output。实际上,这种情况很少见(并且取决于操作系统),但是健壮的驱动程序必须能够处理这种情况。

  • char *driver_name - 驱动程序名称。它必须与 erlang:open_port/2 中使用的原子以及驱动程序库文件的名称(不带扩展名)相对应。

  • void (*finish)(void) - 在卸载驱动程序时由 erl_ddll 驱动程序调用。(它仅在动态驱动程序中调用。)

    仅在调用 erl_ddll:unload_driver/1 或模拟器停止运行时才会卸载驱动程序。

  • void *handle - 此字段保留供模拟器内部使用。模拟器将修改此字段,因此 driver_entry 不声明为 const 非常重要。

  • ErlDrvSSizeT (*control)(ErlDrvData drv_data, unsigned int command, char *buf, ErlDrvSizeT len, char **rbuf, ErlDrvSizeT rlen) - 使用 erlang:port_control/3 调用的特殊例程。它的工作方式有点像 Erlang 驱动程序的“ioctl”。指定给 port_control/3 的数据到达 buflen 中。驱动程序可以使用 *rbufrlen 发回数据。

    这是调用驱动程序并获取响应的最快方法。它不会在 Erlang 模拟器中进行上下文切换,也不需要消息传递。它适用于调用 C 函数以获得更快的执行速度,当 Erlang 太慢时。

    如果驱动程序想要返回数据,则将其返回到 rbuf 中。调用 control 时,*rbuf 指向一个 rlen 字节的默认缓冲区,该缓冲区可用于返回数据。数据的返回方式因端口控制标志(使用 erl_driver:set_port_control_flags 设置的那些标志)而异。

    如果标志设置为 PORT_CONTROL_FLAG_BINARY,则返回二进制数据。可以通过将原始数据写入默认缓冲区来返回小二进制数据。也可以通过将 *rbuf 设置为指向使用 erl_driver:driver_alloc_binary 分配的二进制数据来返回二进制数据。此二进制数据在 control 返回后会自动释放。驱动程序可以使用 erl_driver:driver_binary_inc_refc 保留二进制数据以供*只读*访问,以便稍后使用 erl_driver:driver_free_binary 释放。永远不允许在 control 返回后更改二进制数据。如果 *rbuf 设置为 NULL,则返回一个空列表。

    如果标志设置为 0,则数据将作为整数列表返回。要么使用默认缓冲区,要么将 *rbuf 设置为指向使用 erl_driver:driver_alloc 分配的更大的缓冲区。缓冲区在 control 返回后会自动释放。

    如果返回的字节数超过几个字节,则使用二进制数据会更快。

    返回值是在 *rbuf 中返回的字节数。

  • void (*timeout)(ErlDrvData drv_data) - 在驱动程序的计时器达到 0 之后的任何时间调用。计时器使用 erl_driver:driver_set_timer 激活。驱动程序之间不存在优先级或顺序,因此如果多个驱动程序同时超时,则会首先调用其中一个驱动程序。

  • void (*outputv)(ErlDrvData drv_data, ErlIOVec *ev) - 当端口被写入时调用。如果为 NULL,则改为调用 output 函数。此函数比 output 更快,因为它直接接收 ErlIOVec,无需复制数据。端口必须处于二进制模式,请参阅 erlang:open_port/2

    ErlIOVec 包含一个适用于 writevSysIOVec 以及一个或多个二进制数据。如果驱动程序从 outputv 返回时需要保留这些二进制数据,则可以将它们排队(例如,使用 erl_driver:driver_enq_bin),或者,如果它们保存在静态或全局变量中,则可以递增引用计数器。

  • void (*ready_async)(ErlDrvData drv_data, ErlDrvThreadData thread_data) - 异步调用完成后调用。异步调用使用 erl_driver:driver_async 启动。此函数从 Erlang 仿真器线程调用,而不是从某个线程(如果启用了多线程)调用的异步函数。

  • void (*flush)(ErlDrvData drv_data) - 当端口即将关闭,并且驱动程序队列中有必须在调用 'stop' 之前刷新的数据时调用。

  • ErlDrvSSizeT (*call)(ErlDrvData drv_data, unsigned int command, char *buf, ErlDrvSizeT len, char **rbuf, ErlDrvSizeT rlen, unsigned int *flags) - 从 erlang:port_call/3 调用。它的工作方式很像 control 回调,但使用外部项格式进行输入和输出。

    command 是一个整数,从 Erlang 的调用中获取(erlang:port_call/3 的第二个参数)。

    buflen 提供调用的参数(erlang:port_call/3 的第三个参数)。可以使用 ei 函数对其进行解码。

    rbuf 指向一个返回缓冲区,长度为 rlen 字节。返回数据必须是外部(二进制)格式的有效 Erlang 项。这会被转换为一个 Erlang 项,并由 erlang:port_call/3 返回给调用者。如果返回数据需要的空间多于 rlen 字节,则可以将 *rbuf 设置为使用 erl_driver:driver_alloc 分配的内存。此内存会在 call 返回后自动释放。

    返回值是在 *rbuf 中返回的字节数。如果返回 ERL_DRV_ERROR_GENERAL(或实际上任何 < 0 的值),则 erlang:port_call/3 将抛出 BAD_ARG

  • void (*event)(ErlDrvData drv_data, ErlDrvEvent event, ErlDrvEventData event_data) - 有意未记录。

  • int extended_marker - 此字段必须等于 ERL_DRV_EXTENDED_MARKER0。旧驱动程序(不了解扩展驱动程序接口)应将此字段设置为 0。如果此字段为 0,则以下所有字段也必须0,或者如果是指针字段,则为 NULL

  • int major_version - 如果字段 extended_marker 等于 ERL_DRV_EXTENDED_MARKER,则此字段必须等于 ERL_DRV_EXTENDED_MAJOR_VERSION

  • int minor_version - 如果字段 extended_marker 等于 ERL_DRV_EXTENDED_MARKER,则此字段必须等于 ERL_DRV_EXTENDED_MINOR_VERSION

  • int driver_flags - 此字段用于将驱动程序功能和其他信息传递给运行时系统。如果字段 extended_marker 等于 ERL_DRV_EXTENDED_MARKER,则它必须包含 0 或按位或运算的驱动程序标志(ERL_DRV_FLAG_*)。存在以下驱动程序标志

    • ERL_DRV_FLAG_USE_PORT_LOCKING - 运行时系统在执行此驱动程序的所有端口上使用端口级锁定,而不是驱动程序级锁定。有关更多信息,请参阅 erl_driver

    • ERL_DRV_FLAG_SOFT_BUSY - 标记驱动程序实例可以处理在 output 和/或 outputv 回调中被调用,即使驱动程序实例已将自身标记为忙(请参阅 erl_driver:set_busy_port)。从 ERTS 5.7.4 开始,Erlang 分发使用的驱动程序需要此标志(分发使用的驱动程序始终需要此行为)。

    • ERL_DRV_FLAG_NO_BUSY_MSGQ - 禁用忙碌端口消息队列功能。有关更多信息,请参阅 erl_driver:erl_drv_busy_msgq_limits

    • ERL_DRV_FLAG_USE_INIT_ACK - 当指定此标志时,链接的驱动程序必须使用 erl_driver:erl_drv_init_ack() 手动确认端口已成功启动。这允许实现者在完成一些初始异步初始化后使 erlang:open_portbadarg 退出。

  • void *handle2 - 此字段保留供仿真器内部使用。仿真器会修改此字段,因此 driver_entry 不声明为 const 很重要。

  • void (*process_exit)(ErlDrvData drv_data, ErlDrvMonitor *monitor) - 当受监视的进程退出时调用。drv_data 是与被监视进程的端口关联的数据(使用 erl_driver:driver_monitor_process ),monitor 对应于创建监视器时填充的 ErlDrvMonitor 结构。驱动程序接口函数 erl_driver:driver_get_monitored_process 可以用于检索正在退出的进程的进程 ID,类型为 ErlDrvTermData

  • void (*stop_select)(ErlDrvEvent event, void* reserved) - 代表 erl_driver:driver_select 调用,当可以安全关闭事件对象时调用。

    Unix 上的典型实现是执行 close((int)event)

    参数 reserved 供将来使用,应忽略。

    与大多数其他回调函数相比,stop_select 的调用独立于任何端口。没有 ErlDrvData 参数传递给该函数。不保证持有驱动程序锁或端口锁。调用 driver_select 的端口甚至可以在调用 stop_select 时关闭。但也可能直接由 erl_driver:driver_select 调用 stop_select

    不允许从 stop_select 调用 驱动程序 API 中的任何函数。此严格限制的原因是 stop_select 可以被调用的易失性上下文。

另请参阅

erl_driver(3), erlang, erl_ddll