檢視原始碼 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 個Events
的限制。)若要將此與執行緒和非同步常式搭配使用,請在 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 模擬器中不會進行內容切換,也不需要訊息傳遞。當 Erlang 太慢時,它適用於呼叫 C 函式以取得更快的執行速度。
如果驅動程式想要傳回資料,它應該將資料傳回至
rbuf
。當呼叫control
時,*rbuf
會指向預設緩衝區rlen
位元組,該緩衝區可用於傳回資料。資料的傳回方式會因埠控制旗標(使用erl_driver:set_port_control_flags
設定的旗標)而有所不同。如果 flag 設定為
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
,則會回傳一個空列表。如果 flag 設定為
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
或以位元方式 OR 運算的驅動程式旗標 (ERL_DRV_FLAG_*
)。存在以下驅動程式旗標ERL_DRV_FLAG_USE_PORT_LOCKING
- 執行此驅動程式的所有埠,執行階段系統會使用埠層級鎖定,而不是驅動程式層級鎖定。如需更多資訊,請參閱erl_driver
。ERL_DRV_FLAG_SOFT_BUSY
- 標示驅動程式實例即使在驅動程式實例將自己標記為忙碌時(請參閱erl_driver:set_busy_port
),也可以處理在output
和/或outputv
回呼中呼叫的情況。從 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
可用於以ErlDrvTermData
形式擷取退出處理序的處理序 ID。void (*stop_select)(ErlDrvEvent event, void* reserved)
- 在可以安全關閉事件物件時,代表erl_driver:driver_select
呼叫。在 Unix 上,典型的實作方式是執行
close((int)event)
。引數
reserved
供未來使用,應忽略。與大多數其他回呼函數不同,
stop_select
的呼叫與任何埠無關。沒有ErlDrvData
引數傳遞給函數。不保證持有驅動程式鎖定或埠鎖定。呼叫driver_select
的埠甚至可以在呼叫stop_select
時關閉。但也可能stop_select
是由erl_driver:driver_select
直接呼叫。不允許從
stop_select
呼叫 驅動程式 API 中的任何函數。這種嚴格限制的原因是stop_select
可能被呼叫的不穩定環境。