非常教程

Erlang 20参考手册

指南:教程 | Guide: Tutorial

6.端口驱动 | 6. Port Drivers

本节概述了如何Problem Example使用链接的端口驱动程序解决示例问题的示例。

端口驱动程序是一个链接的驱动程序,可以从Erlang程序中作为端口访问。它是一个共享库(UNIX中的SO,Windows中的DLL),带有特殊的入口点。Erlang运行时系统在启动驱动程序和将数据发送到端口时调用这些入口点。端口驱动程序也可以发送数据给Erlang。

由于端口驱动程序是动态链接到仿真程序的,所以这是从Erlang调用C代码的最快方法。在端口驱动程序中调用函数不需要上下文切换。但它也是最不安全的方式,因为端口驱动程序崩溃也会导致仿真器失效。

该方案如下图所示:

图6.1:端口驱动程序通信

6.1 Erlang程序

像端口程序一样,端口与Erlang进程通信。所有的通信都通过一个Erlang进程,这个进程是端口驱动程序的连接进程。终止此过程将关闭端口驱动程序。

在创建端口之前,必须加载驱动程序。这是通过函数完成的erl_dll:load_driver/1,共享库的名称作为参数。

然后使用BIF创建端口open_port/2,并将元组{spawn, DriverName}作为第一个参数。该字符串SharedLib是端口驱动程序的名称。第二个参数是选项列表,在这种情况下是none:

-module(complex5).
-export([start/1, init/1]).

start(SharedLib) ->
    case erl_ddll:load_driver(".", SharedLib) of
        ok -> ok;
        {error, already_loaded} -> ok;
        _ -> exit({error, could_not_load_driver})
    end,
    spawn(?MODULE, init, [SharedLib]).

init(SharedLib) ->
  register(complex, self()),
  Port = open_port({spawn, SharedLib}, []),
  loop(Port).

现在complex5:foo/1complex5:bar/1可以实现。两者都向该complex流程发送消息并收到以下回复:

foo(X) ->
    call_port({foo, X}).
bar(Y) ->
    call_port({bar, Y}).

call_port(Msg) ->
    complex ! {call, self(), Msg},
    receive
        {complex, Result} ->
            Result
    end.

complex过程执行以下操作:

  • 将消息编码为一系列字节。
  • 将其发送到端口。
  • 等待回复。
  • 解码答复。
  • 将其发回给调用者:
loop(Port) ->
    receive
        {call, Caller, Msg} ->
            Port ! {self(), {command, encode(Msg)}},
            receive
                {Port, {data, Data}} ->
                    Caller ! {complex, decode(Data)}
            end,
            loop(Port)
    end.

假设C函数的参数和结果都小于256,则采用简单的编码/解码方案。在这个方案中,foo由字节1 bar表示,由2表示,并且参数/结果也由单个字节表示:

encode({foo, X}) -> [1, X];
encode({bar, Y}) -> [2, Y].

decode([Int]) -> Int.

生成的Erlang程序包括停止端口和检测端口故障的功能如下:

-module(complex5).
-export([start/1, stop/0, init/1]).
-export([foo/1, bar/1]).

start(SharedLib) ->
    case erl_ddll:load_driver(".", SharedLib) of
	ok -> ok;
	{error, already_loaded} -> ok;
	_ -> exit({error, could_not_load_driver})
    end,
    spawn(?MODULE, init, [SharedLib]).

init(SharedLib) ->
    register(complex, self()),
    Port = open_port({spawn, SharedLib}, []),
    loop(Port).

stop() ->
    complex ! stop.

foo(X) ->
    call_port({foo, X}).
bar(Y) ->
    call_port({bar, Y}).

call_port(Msg) ->
    complex ! {call, self(), Msg},
    receive
	{complex, Result} ->
	    Result
    end.

loop(Port) ->
    receive
	{call, Caller, Msg} ->
	    Port ! {self(), {command, encode(Msg)}},
	    receive
		{Port, {data, Data}} ->
		    Caller ! {complex, decode(Data)}
	    end,
	    loop(Port);
	stop ->
	    Port ! {self(), close},
	    receive
		{Port, closed} ->
		    exit(normal)
	    end;
	{'EXIT', Port, Reason} ->
	    io:format("~p ~n", [Reason]),
	    exit(port_terminated)
    end.

encode({foo, X}) -> [1, X];
encode({bar, Y}) -> [2, Y].

decode([Int]) -> Int.

6.2 C驱动器

C驱动程序是一个编译并链接到共享库的模块。它使用驱动程序结构并包含头文件erl_driver.h

驱动程序结构填充了驱动程序名称和函数指针。它是从宏指定的特殊入口点返回的DRIVER_INIT(<driver_name>)

接收和发送数据的功能被组合成一个由驱动器结构指出的功能。发送到端口的数据作为参数给出,并且回复的数据与C函数一起发送driver_output

由于驱动程序是共享模块,而不是程序,因此不存在主要功能。在这个例子中,所有函数指针都没有使用,并且driver_entry结构中的相应字段被设置为NULL。

驱动程序中的所有函数都接收一个start刚刚由Erlang进程传递的句柄(返回)。这必须以某种方式引用端口驱动程序实例。

example_drv_start是唯一被调用的端口实例句柄,因此必须保存。习惯上使用一个分配的驱动程序定义的结构,并将指针传回作为参考。

使用全局变量并不是一个好主意,因为端口驱动程序可以由多个Erlang进程产生。这个驱动程序结构将被多次实例化:

/* port_driver.c */

#include <stdio.h>
#include "erl_driver.h"

typedef struct {
    ErlDrvPort port;
} example_data;

static ErlDrvData example_drv_start(ErlDrvPort port, char *buff)
{
    example_data* d = (example_data*)driver_alloc(sizeof(example_data));
    d->port = port;
    return (ErlDrvData)d;
}

static void example_drv_stop(ErlDrvData handle)
{
    driver_free((char*)handle);
}

static void example_drv_output(ErlDrvData handle, char *buff, 
			       ErlDrvSizeT bufflen)
{
    example_data* d = (example_data*)handle;
    char fn = buff[0], arg = buff[1], res;
    if (fn == 1) {
      res = foo(arg);
    } else if (fn == 2) {
      res = bar(arg);
    }
    driver_output(d->port, &res, 1);
}

ErlDrvEntry example_driver_entry = {
    NULL,			/* F_PTR init, called when driver is loaded */
    example_drv_start,		/* L_PTR start, called when port is opened */
    example_drv_stop,		/* F_PTR stop, called when port is closed */
    example_drv_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 */
    "example_drv",		/* 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 */
};

DRIVER_INIT(example_drv) /* must match name in driver_entry */
{
    return &example_driver_entry;
}

6.3运行示例

第一步。编译C代码:

unix> gcc -o example_drv.so -fpic -shared complex.c port_driver.c
windows> cl -LD -MD -Fe example_drv.dll complex.c port_driver.c

第二步。启动Erlang并编译Erlang代码:

> erl
Erlang (BEAM) emulator version 5.1

Eshell V5.1 (abort with ^G)
1> c(complex5).
{ok,complex5}

第三步。运行示例:

2> complex5:start("example_drv").
<0.34.0>
3> complex5:foo(3).
4
4> complex5:bar(5).
10
5> complex5:stop().
stop
Erlang 20

Erlang 是一种通用的面向并发的编程语言,可应付大规模开发活动的程序设计语言和运行环境。

主页 https://www.erlang.org/
源码 https://github.com/erlang/otp
版本 20
发布版本 20.1