Erlang/Elixir: 外部通訊之-端口驅動

系列:
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']}

端口驅動的資料

端口驅動的例子

驅動入口

驅動入口是一個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文件便可.

編寫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_hookspost_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, 包含 foobar的函數接口聲明, 並在 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...

相關文章
相關標籤/搜索