Erlang/Elixir: 外部通信之-端口驱动

猫咪的晴天 2019-06-20

系列:
Erlang/Elixir: 外部通信之-NIF
Erlang/Elixir: 外部通信之-端口驱动
Erlang/Elixir: 外部通信之-C节点

本文是Erlang/Elixir和外部世界通信的第一篇, 阐述了端口驱动的基本概念以及和外部世界的通信方式, 目前主要有如下几种方式

  • NIF

  • Port

  • Port driver

  • C node

这一篇文章从端口驱动开始讲起, 后续的文章我会介绍其他的几种方式.

端口驱动和Erlang VM的关系
Erlang/Elixir: 外部通信之-端口驱动

概述

来看看端口驱动的定义

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.

  • 首先, 它是一个可以从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...

相关推荐