系列:
Erlang/Elixir: 外部通訊之-NIF
Erlang/Elixir: 外部通訊之-端口驅動
Erlang/Elixir: 外部通訊之-C節點html本文是Erlang/Elixir和外部世界通訊的第一篇, 闡述了端口驅動的基本概念以及和外部世界的通訊方式, 目前主要有以下幾種方式node
NIFlinux
Portgit
Port drivergithub
C nodeshell
這一篇文章從端口驅動開始講起, 後續的文章我會介紹其餘的幾種方式.segmentfault
端口驅動和Erlang VM的關係安全
來看看端口驅動的定義數據結構
A port driver is a linked-in driver that is accessible as a port from an Erlang program. It is a shared library (SO in UNIX, DLL in Windows), with special entry points.async
首先, 它是一個能夠從Erlang程序中訪問的端口.
其次, 它是一個有特殊入口點的共享庫.
當驅動啓動,而且當數據被髮送到這些端口時, Erlang運行時系統調用這些特殊的入口點.
動態加載到Erlang運行時, 是運行C代碼最快的方式之一
函數調用不須要上下文切換
和NIF同樣, 是最不安全的方式之一, 端口驅動的崩潰會致使整個Erlang VM崩潰.
和端口程序同樣, 經過一個端口和Erlang進行(鏈接進程)通訊, 鏈接進程終止, 端口自動關閉.
建立端口以前, 必須首先加載端口驅動
端口驅動是經過 erl_dll:load_driver/1
加載的, 使用共享庫的路徑做爲其參數.
端口經過open_port/2
建立, 以元組 {spawn, DriverName}
做爲第一個參數, 字符串SharedLib
爲端口驅動的名稱, 第二個參數爲選項列表.
注意driver中的函數都是靜態的, 靜態函數與普通函數不一樣, 它只能在聲明它的文件當中可見, 不能被其它文件使用.
靜態函數會被自動分配在一個一直使用的存儲區,直到退出應用程序實例,避免了調用函數時壓棧出棧,速度快不少
Erlang 運行時自己有部分東西就是經過端口驅動實現的, 咱們能夠經過以下命令來查看系統提供的那些驅動
iex(1)> :erl_ddll.loaded_drivers {:ok, ['efile', 'tcp_inet', 'udp_inet', 'zlib_drv', 'ram_file_drv', 'tty_sl']}
端口驅動的資料
端口驅動的例子
通用Erlang端口和端口驅動
使用一個單一自包含的文件自動生成Erlang端口驅動或Erlang端口的C/C++綁定.
驅動入口是一個C結構體 ErlDrvEntry
, ErlDrvEntry
結構的詳細定義 http://erlang.org/doc/man/dri...
其中咱們能夠經過源碼知道, 驅動啓動時的回調函數有兩種函數簽名, 一種是給開發者開發自定義驅動的, 另外一種是給系統驅動的, 咱們以前說了, Erlang內置的端口驅動能夠經過:erl_ddll.loaded_drivers/0
的輸出看到.
#ifndef ERL_SYS_DRV /* called when open_port/2 is invoked. return value -1 means failure. */ ErlDrvData (*start)(ErlDrvPort port, char *command); #else /* special options, only for system driver */ ErlDrvData (*start)(ErlDrvPort port, char *command, SysDriverOpts* opts); #endif
下面咱們以一個官方文檔中的一個例子來講明如何編寫一個端口驅動. 這個例子經過在C代碼中實現一個加法函數和一個乘法函數
complex.c
int foo(int x) { return x+1; } int bar(int y) { return y*2; }
這一部分闡述若是從頭開始建立一個端口驅動.
首先你須要安裝 rebar3, 經過rebar3 new lib c_portdriver
建立一個項目.
rebar3 new lib c_portdriver
這個命令建立了一個 c_src/Makefile
文件用於編譯C代碼, 其中定義了幾個關鍵的變量, 用於獲取 erl_driver.h
頭文件的路徑
ERTS_INCLUDE_DIR ?= $(shell erl -noshell -s init stop -eval "io:format(\"~s/erts-~s/include/\", [code:root_dir(), erlang:system_info(version)]).") ERL_INTERFACE_INCLUDE_DIR ?= $(shell erl -noshell -s init stop -eval "io:format(\"~s\", [code:lib_dir(erl_interface, include)]).") ERL_INTERFACE_LIB_DIR ?= $(shell erl -noshell -s init stop -eval "io:format(\"~s\", [code:lib_dir(erl_interface, lib)]).")
這樣咱們只須要在 c_src
目錄中存放實現端口驅動須要的C文件便可.
頭文件
complex.h
/* complex.h */ int foo(int); int bar(int);
邏輯實現
complex.c
#include "complex.h" int foo(int x) { return x+1; } int bar(int y) { return y*2; }
驅動封裝
c_portdriver.c
#include <stdio.h> #include "erl_driver.h" #include "complex.h" typedef struct { // 端口句柄, 用於和Erlang進程通訊 ErlDrvPort port; } example_data_t; // 端口打開回調 static ErlDrvData c_portdriver_start(ErlDrvPort port, char *buff) { // 給分 example_data_t 結構配內存 example_data_t* d = (example_data_t*) driver_alloc(sizeof(example_data_t)); // 設置端口 d->port = port; // 返回數據結構 return (ErlDrvData)d; } // 端口關閉回調 static void c_portdriver_stop(ErlDrvData handle) { // 釋放內存 driver_free((char*)handle); } // 消息發送回調, 當Erlang 向 Port 發送消息時 static void c_portdriver_output(ErlDrvData handle, char *buff, ErlDrvSizeT bufflen) { example_data_t* d = (example_data_t*)handle; char fn = buff[0], arg = buff[1], res; // 調用complex.c文件中的函數 if (fn == 1) { res = foo(arg); } else if (fn == 2) { res = bar(arg); } // 把返回值發送給Erlang VM, 經過端口發送給在Erlang VM中的連接進程 driver_output(d->port, &res, 1); } ErlDrvEntry example_driver_entry = { NULL, /* F_PTR init, called when driver is loaded */ c_portdriver_start, /* L_PTR start, called when port is opened */ c_portdriver_stop, /* F_PTR stop, called when port is closed */ c_portdriver_output, /* F_PTR output, called when erlang has sent */ NULL, /* F_PTR ready_input, called when input descriptor ready */ NULL, /* F_PTR ready_output, called when output descriptor ready */ "c_portdriver", /* char *driver_name, the argument to open_port */ NULL, /* F_PTR finish, called when unloaded */ NULL, /* void *handle, Reserved by VM */ NULL, /* F_PTR control, port_command callback */ NULL, /* F_PTR timeout, reserved */ NULL, /* F_PTR outputv, reserved */ NULL, /* F_PTR ready_async, only for async drivers */ NULL, /* F_PTR flush, called when port is about to be closed, but there is data in driver queue */ NULL, /* F_PTR call, much like control, sync call to driver */ NULL, /* F_PTR event, called when an event selected by driver_event() occurs. */ ERL_DRV_EXTENDED_MARKER, /* int extended marker, Should always be set to indicate driver versioning */ ERL_DRV_EXTENDED_MAJOR_VERSION, /* int major_version, should always be set to this value */ ERL_DRV_EXTENDED_MINOR_VERSION, /* int minor_version, should always be set to this value */ 0, /* int driver_flags, see documentation */ NULL, /* void *handle2, reserved for VM use */ NULL, /* F_PTR process_exit, called when a monitored process dies */ NULL /* F_PTR stop_select, called to close an event object */ }; // 驅動結構用驅動名稱和函數指針填充, 它使用驅動結構, 而且包含頭文件 erl_driver.h DRIVER_INIT(c_portdriver) /* must match name in driver_entry */ { return &example_driver_entry; }
Erlang 端的實現, 能夠參考代碼庫中的代碼: https://github.com/developerw...
若是要使用 rebar3 compile
編譯C代碼, 須要修改在rebar.config
文件中增長兩個配置pre_hooks
和post_hooks
, 參考: http://www.rebar3.org/docs/bu...
{erl_opts, [ debug_info, warnings_as_errors ]}. {deps, []}. {pre_hooks, [ {"(linux|darwin|solaris)", compile, "make -C c_src"}, {"(freebsd)", compile, "gmake -C c_src"} ]}. {post_hooks, [ {"(linux|darwin|solaris)", clean, "make -C c_src clean"}, {"(freebsd)", clean, "gmake -C c_src clean"} ]}.
➜ c_portdriver rebar3 compile ===> Verifying dependencies... ===> Compiling c_portdriver cc -O3 -std=gnu99 -arch x86_64 -Wall -Wmissing-prototypes -fPIC -I /Users/hezhiqiang/.kerl/installs/18.3_dirty_schedulers/erts-7.3/include/ -I /Users/hezhiqiang/.kerl/installs/18.3_dirty_schedulers/lib/erl_interface-3.8.2/include -c -o /Users/hezhiqiang/tmp/erlang_ffi/portdrivers/c_portdriver/c_src/c_portdriver.o /Users/hezhiqiang/tmp/erlang_ffi/portdrivers/c_portdriver/c_src/c_portdriver.c cc -O3 -std=gnu99 -arch x86_64 -Wall -Wmissing-prototypes -fPIC -I /Users/hezhiqiang/.kerl/installs/18.3_dirty_schedulers/erts-7.3/include/ -I /Users/hezhiqiang/.kerl/installs/18.3_dirty_schedulers/lib/erl_interface-3.8.2/include -c -o /Users/hezhiqiang/tmp/erlang_ffi/portdrivers/c_portdriver/c_src/complex.o /Users/hezhiqiang/tmp/erlang_ffi/portdrivers/c_portdriver/c_src/complex.c cc /Users/hezhiqiang/tmp/erlang_ffi/portdrivers/c_portdriver/c_src/c_portdriver.o /Users/hezhiqiang/tmp/erlang_ffi/portdrivers/c_portdriver/c_src/complex.o -arch x86_64 -flat_namespace -undefined suppress -shared -L /Users/hezhiqiang/.kerl/installs/18.3_dirty_schedulers/lib/erl_interface-3.8.2/lib -lerl_interface -lei -o /Users/hezhiqiang/tmp/erlang_ffi/portdrivers/c_portdriver/c_src/../priv/c_portdriver.so
1. 在C99中隱含的函數 'bar' 聲明無效.
c_portdriver.c:39:13: warning: implicit declaration of function 'foo' is invalid in C99上面的警告是由於, 咱們在設置編譯器選項的時候增長了 --std=c99 選項. 它是在 Makefile文件中定義的
UNAME_SYS := $(shell uname -s) ifeq ($(UNAME_SYS), Darwin) CC ?= cc CFLAGS ?= -O3 -std=c99 -arch x86_64 -finline-functions -Wall -Wmissing-prototypes CXXFLAGS ?= -O3 -arch x86_64 -finline-functions -Wall LDFLAGS ?= -arch x86_64 -flat_namespace -undefined suppress else ifeq ($(UNAME_SYS), FreeBSD) CC ?= cc CFLAGS ?= -O3 -std=c99 -finline-functions -Wall -Wmissing-prototypes CXXFLAGS ?= -O3 -finline-functions -Wall else ifeq ($(UNAME_SYS), Linux) CC ?= gcc CFLAGS ?= -O3 -std=c99 -finline-functions -Wall -Wmissing-prototypes CXXFLAGS ?= -O3 -finline-functions -Wall endif解決辦法(二選一,推薦第一種):
建立頭文件
complex.h
, 包含foo
和bar
的函數接口聲明, 並在c_portdriver.c
包含進來, 可消除此警告.刪除
-std=c99
編譯選項2. 關於OSX下面的警告
clang: warning: optimization flag '-finline-functions' is not supported解決辦法:
找到 Makefile 中的ifeq ($(UNAME_SYS), Darwin)
行, 把Darwin編譯器選的-finline-functions
去掉.3. 沒有定義函數原型(接口)
complex.c:2:5: warning: no previous prototype for function 'foo'解決辦法
把complex.h
頭文件包含到complex.c
中.
這裏由於是經過 rebar3
建立的項目, 須要使用 rebar3 shell
才能正確的加載路徑.
➜ rebar3 shell ... Eshell V7.3 (abort with ^G) 1> c_portdriver:start("c_portdriver"). <0.104.0> 2> c_portdriver: bar/1 foo/1 init/1 module_info/0 module_info/1 start/1 stop/0 2> c_portdriver:foo(1). 2 3> c_portdriver:bar(5). 10 4>
https://github.com/developerw...
動態庫是在OSX下編譯的, 若是你是其餘平臺, 可執行 rebar3 clean
, rebar3 compile
從新生成動態庫文件.
這篇文章只是一個基本的概述, 和上手指南, 要開發一個有用的, 完整的端口驅動, 咱們還須要熟悉端口驅動的不少數據接口和內置函數. 能夠參考這裏: http://erlang.org/doc/man/erl...