查看源代码 driver_entry
Erlang 驱动程序使用的 driver-entry 结构。
描述
警告
请极其小心地使用此功能。
驱动程序回调函数作为 VM 本地代码的直接扩展执行。执行不是在安全的环境中进行的。VM *不能*提供与执行 Erlang 代码时提供的相同服务,例如抢占式调度或内存保护。如果驱动程序回调函数行为不正常,整个 VM 将会行为异常。
- 发生崩溃的驱动程序回调函数将导致整个 VM 崩溃。
- 错误实现的驱动程序回调函数可能会导致 VM 内部状态不一致,这可能会导致 VM 崩溃,或者在调用驱动程序回调函数之后的任何时刻发生 VM 的其他各种异常行为。
- 在返回之前执行长时间工作的驱动程序回调函数会降低 VM 的响应速度,并可能导致各种奇怪的行为。这些奇怪的行为包括但不限于极端的内存使用以及调度器之间的不良负载平衡。由于长时间工作而可能发生的奇怪行为也可能因 Erlang/OTP 版本而异。
从 ERTS 5.9(Erlang/OTP R15B)开始,驱动程序接口已更改,回调函数 output
、control
和 call
的类型更大。请参阅 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_entry
为const
。这是因为模拟器必须修改handle
和handle2
字段。静态分配的、声明为const
的driver_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/1
或Port ! {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/2
和erlang:port_command/2
。void (*ready_input)(ErlDrvData drv_data, ErlDrvEvent event)
void (*ready_output)(ErlDrvData drv_data, ErlDrvEvent event)
- 当驱动程序事件(在参数event
中指定)发出信号时调用。这用于帮助异步驱动程序在发生某些事情时“唤醒”。在 Unix 上,
event
是管道或套接字句柄(或select
系统调用可以理解的东西)。在 Windows 上,
event
是Event
或Semaphore
(或WaitForMultipleObjects
API 函数可以理解的东西)。(模拟器中的一些技巧允许使用超过内置的 64 个Event
限制。)要将此与线程和异步例程一起使用,请在 Unix 上创建管道,在 Windows 上创建
Event
。当例程完成时,写入管道(在 Windows 上使用SetEvent
),这会使模拟器调用ready_input
或ready_output
。可能会发生虚假事件。也就是说,尽管没有发出真正的事件信号,也会调用
ready_input
或ready_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
的数据到达buf
和len
中。驱动程序可以使用*rbuf
和rlen
发回数据。这是调用驱动程序并获取响应的最快方法。它不会在 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
包含一个适用于writev
的SysIOVec
以及一个或多个二进制数据。如果驱动程序从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
的第二个参数)。buf
和len
提供调用的参数(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_MARKER
或0
。旧驱动程序(不了解扩展驱动程序接口)应将此字段设置为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_port
以badarg
退出。
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
可以被调用的易失性上下文。