Erlang과 Python 인터페이싱하기

몇일전 Erlang이 라이브러리가 부족하다 뭐하다 하는 썰을 풀어 놓았었다. 그러다 어제 돌입한 작업이 그럼 Erlang과 다른 외계어들간에 인터페이싱이였다.

작업은 얼랭에서 강력하게 추천하는 방법인 port를 통한 인터페이싱이다. 강력하게 추천하는 이유는 다른 언어로 된 프로세스가 얼랭 런타임 환경 이외에서 실행이 되고 그곳에서의 예기치 못한 fail이 전체 얼랭 런타임 환경의 fail로 이어지는것을 방지해 주기 때문이다. 한마디로 fault-tolerant 한 시스템을 위해 이런 방법을 추천하는 것이다.
그러기 때문에 인터페이싱에 약간의 성능저하가 있을 수 있지만 나도 이 방법을 추천한다. 그게 얼랭을 쓰는 이유와 맞기 때문이다.

이 방법은 프로그래밍 얼랭(Programming Erlang)에서 한개의 챕터로 설명이 되고 있으나, 내용상 상당히 빈약했다.
아무래도 데이터 디코드나 인코딩이 전송되는 데이터 타입에 따라 바이수준이나 비트 수준의 것으로 로레벨로 내려가기 때문인거 같다는 생각을 해본다.

먼저 얼랭이 외부 프로그램과 port를 통해서 통신하는 방식은 첫번째 데이터에 big-endian으로 데이터 길이를 명시한다. 이는 2바이트나 4바이트 정보로 기재가 가능하다.
따라서 우리가 double이라는 함수를 외부 python 프로세스에서 실행 결과를 받기 위해 [0,2,0,4] 이와 같은 시퀀스를 전송하게 된다.
이는 2바이트 길이 정보를 가지고 있으며, 0번째 함수를 실행해 4의 인자를 전달한다는 의미이다.(각 1바이트)
이와 같은 정보를 python 프로세스는 stdin으로 받게 되어 파싱해 해당 함수를 실행한 결과를 stdout으로 보내주면 된다.
double이라는 함수가 실행된 결과 [0,1,8]의 데이터가 전송이 될 것이다. 이는 길이 1인 데이터인 결과 8이 전송됨을 의미한다.

위는 단지 1바이트씩의 정보를 전달하는것에 불과하다. 만일 인자로 4바이트 int형이 전달된다면 python 프로세스에서 정보를 받을때 정확하게 4바이트 int로 파싱해야 된다. 이를 위해 struct 모듈의 pack, unpack에 대한 이해가 필수적이다. (struct 모듈은 C의 구조체와 인터페이싱 하기 위한 모듈이다. 하지만 얼랭에서도 C구조체처럼 packing해서 데이터를 보내는게 가능하니 이걸 사용하지 않을 이유가 없다.)
게다가 얼랭 port에서 전송되는 데이터는 big-endian으로 전송이 되기 때문에 이에 대해 정확하게 받을 수 있도록 파싱해야 된다.

아래는 얼랭 코드이다.

[CODE C]
-module(port_test).
-export([start/0, stop/0]).
-export([double/1, sum/2, gethtml/1]).

start() ->
    spawn(fun() ->
          register(example1, self()),
          process_flag(trap_exit, true),
          Port = open_port({spawn, “python test.py”}, [{packet, 4}]),
          loop(Port)
      end).

stop() ->
    example1 ! stop.

double(X) -> call_port({double, X}).
sum(X,Y) -> call_port({sum, X, Y}).
gethtml(X) ->
    Html = call_port({gethtml, X}),
    io:format(“~s”, [Html]).

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

loop(Port) ->
    receive
    {call, Caller, Msg} ->
        Port ! {self(), {command, encode(Msg)}},
        receive
        {Port, {data, Data}} ->
            Caller ! {example1, decode(Data)}
        end,
        loop(Port);
    stop ->
        Port ! {self(), close},
        receive
        {Port, closed} ->
            exit(normal)
        end;
    {‘EXIT’, Port, Reason} ->
        exit({port_terminated,Reason})
    end.

encode({double, X}) ->
    NewX = <<X:32>>,
    [1, NewX];  
encode({sum, X, Y}) ->
    [0, <<X:32, Y:32>>];
encode({gethtml, X}) ->
    [2, list_to_binary(X)].

decode(Data) ->
    case size(list_to_binary(Data)) of
        Size when Size == 4 ->
            <<Int:32>> = list_to_binary(Data),
            Int;
        Size when Size > 4 ->
            Data;
        true ->
            io:format(“some error occurs!~n”)
    end.
[/CODE]

위 코드에는 double, sum, gethtml 함수에 대한 인터페이싱 코드가 들어있다.
주목해야 될 부분은 encode와 decode부분인데, 각 함수 식별자 0,1,2와 함께 Bit syntax로 표현이 되어 있다.
<<X:32>>는 X라는 변수를 32bit로 팩킹해서 binary데이터로 만든다는 의미이다. 이때 기본적으로 Big-endian으로 패킹이 된다.

gethtml에는 url 인자가 넘겨지는데, 이는 string이기 때문에 list_to_binary함수로 간단하게 binary데이터로 변환을 할 수 있다.

decode부분도 주목할 부분인데, 받아온 데이터의 사이즈가 int형 그러니까 4바이트 이면 Int형변환을 실시하고, 그것보다 큰 데이터가 오면 string데이터로 간주하고 그대로 반환하는 코드이다.
string 데이터는 python에서 gethtml에 의해 리턴된 결과가 되겠다.

그럼 여기 Python 코드를 올려본다.

[CODE]
#!/usr/bin/python

import sys
from struct import *
import urllib2

def read_cmd():
     L = sys.stdin.read(4)
    if L == “”:
        return None
    (Len,) = unpack(‘>i’, L)
    Buf = sys.stdin.read(Len)
    if Buf == “”:
        return None
    return Buf

def write_cmd(Res):
    p1 = pack(‘>i’, 4)
    p2 = pack(‘>i’, Res)
    sys.stdout.write(p1)
    sys.stdout.write(p2)
    sys.stdout.flush()

def write_string(Str):
    l = len(Str)
    p1 = pack(‘>i’, l)
    p2 = pack(“>” + str(l) + “s”, Str)
    sys.stdout.write(p1)
    sys.stdout.write(p2)
    sys.stdout.flush()

#0
def sum(X, Y):
    return X + Y
#1
def double(X):
    return 2 * X
#2
def gethtml(url):
    urlf = urllib2.urlopen(url)
    return urlf.read()

if __name__ == “__main__”:
    Buf = read_cmd()    

    while Buf != None:
        (F,) = unpack(‘>B’, Buf[0])

        if F == 0:
            (X, Y) = unpack(‘>2i’, Buf[1:])
            write_cmd(sum(X, Y))
        elif F == 1:
            (X, ) = unpack(‘>i’, Buf[1:5])
            write_cmd(double(X))
        elif F == 2:
            l = len(Buf[1:])
            (X, ) = unpack(“>” + str(l) + “s”, Buf[1:])
            write_string(gethtml(X))
        else:
            sys.stdin.write(“some error occur!\n”)
        Buf = read_cmd()
[/CODE]

struct 모듈의 pack과 unpack의 의미만 안다면 크게 어렵지 않게 이해할수 있는 코드이다.

여기서 중요한점은 port에서 나오는 데이터와 받는 데이터는 모두 big-endian이기 때문에 이에 대한 변환이 필수적이라는 것이다.
그래서 길이정보인 4바이트를 받고 이를 int형으로 변환하기 위해 아래의 코드가 쓰인다.

(Len,) = unpack(‘>i’, L)

‘>’는 big-endian 변환을 한다는 것이고, ‘i’는 4바이트 int형을 의미한다.

모든 과정이 stdin과 stdout으로 이루어지니 디버깅을 할때는 stderr를 이용하면 되겠다.

요 port라는 놈이 얼랭에서는 중요하게 쓰이므로 반드시 이해할 필요가 있는 놈인거 같다.
게다가 위와 같은 식의 길이를 명시한 메시지 패싱뿐만 아니라 newline을 단위로 파싱이 가능하기 때문에 hadoop의 streaming 과 비슷한 동작을 하는 것들도 구현이 가능할 것이라고 예상해본다.

다만 내가 궁금한건, 다수의 노드에서 외부 모듈을 사용할때 이 외부 모듈이 각 노드에 어떤방식으로든 전달이 되어야 되는데 이게 한번에 가능한게 있을가라는 것이다. 물론 각 노드에 물리적으로 copy를 하는 방법이 있겠다. 아주 단순하지만 말이다.

일단 Python이 스트링 처리 라이브러리가 상당히 방대해서  Python으로 구현해 봤다.
위 Python 코드처럼 다른 언어 그러니까  C++, C, Perl, PHP, Java 등등도 위와 같은 방법으로 인터페이싱이 쉽게 가능할 것이다.

ps. 위 모듈이 예외 처리가 안되어 있지만 일단 가장 큰 오류는 4byte로 표현할수 있는 길이 영역을 gethtml코드의 리턴값이 초과할때 생길 수 있을것이다. 이런 자잘한 작업들은 실제 모듈 작성시 반드시 고려되어야 될 부분이 될 것이다.

CC BY-NC 4.0 Erlang과 Python 인터페이싱하기 by from __future__ import dream is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License.