PyPy 임베딩(지원 중단됨)

PyPy는 cffi 사용과 Python이 C보다 더 나은 언어라는 철학에 기반을 둔, 매우 최소한이면서도 매우 특이한 임베딩 인터페이스를 가지고 있습니다. 이 인터페이스는 uwsgi 프로젝트의 Roberto De Ioris와 협력하여 개발되었습니다. PyPy uwsgi plugin은 임베딩 API를 사용하는 좋은 예입니다.

참고: --shared 옵션으로 컴파일된 PyPy, 즉 libpypy*-c.so 또는 pypy*-c.dll 파일이 있는 PyPy가 필요합니다. 이것이 기본값입니다.

Note

이 페이지에서 설명하는 인터페이스는 하위 호환성을 위해 유지되고 있습니다. PyPy 4.1부터는 이 인터페이스 대신 CFFI의 네이티브 임베딩 지원을 사용할 것을 권장합니다. 이 방식은 더 간단하며 PyPy뿐 아니라 CPython에서도 동작합니다.

생성된 공유 라이브러리가 내보내는 함수는 매우 적습니다. 이 함수들은 PyPy.h에 정의되어 있습니다. 이 파일은 v7.3.8에서 제거되었지만 is still available에서 여전히 구할 수 있습니다. 몇 가지 원칙만 지킨다면 이 함수들로 필요한 모든 작업을 수행할 수 있습니다. API는 다음과 같습니다:

void rpython_startup_code(void);

이 함수는 다른 어떤 것보다 먼저 (한 번) 호출해야 하는 함수입니다. 이 함수는 RPython/PyPy GC를 초기화하고, 필요한 여러 시작 코드를 실행합니다. 이 함수는 실패하지 않습니다.

int pypy_setup_home(char* home, int verbose);

이 함수는 주어진 “PyPy 홈 디렉터리”에서 시작하여 PyPy 표준 라이브러리를 검색합니다. 인자는 다음과 같습니다:

  • home: pypy 디렉터리 안에 있는 실행 파일의 경로입니다(.so 이름이어도 되고, 임의로 지어낸 이름이어도 됩니다). 표준 라이브러리를 찾는 데 사용되며, sys.executable로도 설정됩니다. PyPy 5.5부터는 libpypy-c.so/dylib/dll 자체가 이 디렉터리 안에 있기만 하면 여기에 그냥 NULL을 지정해도 됩니다.
  • verbose: 0이 아니면 오류 메시지를 stderr에 출력합니다

이 함수는 성공하면 0을, 실패하면 -1을 반환하며, 라이브러리를 찾을 때까지 여러 번 호출할 수 있습니다.

void pypy_init_threads(void);

스레드를 초기화합니다. 스레드를 사용하는 경우에만 호출하면 됩니다. 반드시 pypy_setup_home() 이후에 호출해야 합니다

int pypy_execute_source(char* source);

source 인자로 주어진 Python 소스 코드를 실행합니다. 예외가 발생하면 Python 트레이스백을 stderr에 출력하고 1을 반환하며, 그렇지 않으면 0을 반환합니다. 오류 처리는 소스 코드 안에서 직접 하는 것이 바람직합니다. 이 함수는 GIL을 획득합니다.

참고: 이 함수는 한 번만 또는 많아야 몇 번만 호출하도록 만들어졌습니다. 아래의 More complete example을 참조하십시오. PyPy <= 2.6.0에서는 전역(globals) 딕셔너리가 여러 호출에 걸쳐 재사용되므로 이상한 결과(예: 객체가 너무 일찍 소멸하는 현상)가 생길 수 있습니다. PyPy >= 2.6.1에서는 호출할 때마다 새 전역 딕셔너리를 받습니다(다만 그 대신 모든 전역 딕셔너리가 sys._pypy_execute_source에 영원히 유지됩니다).

int pypy_execute_source_ptr(char* source, void* ptr);

Note

2014년 6월 PyPy 2.3.1에서 추가되었습니다.

위와 동일하지만, 소스 스코프에 매직 인자를 c_argument로 등록한다는 점이 다릅니다. 이때 void*는 Python int로 인코딩됩니다.

void pypy_thread_attach(void);

애플리케이션이 PyPy 외부에서 초기화된 스레드를 사용하는 경우, 이 함수를 호출하여 PyPy GC가 해당 스레드를 추적하도록 알려야 합니다. 이 함수 자체는 스레드 안전(thread-safe)하지 않으므로 뮤텍스로 가드(guard)해야 합니다.

최소 예제

이 API는 CPython C API 등에 비해 훨씬 더 최소한으로 구성되어 있으므로, 처음에는 할 수 있는 일이 많지 않다고 생각하기 쉽습니다. 그러나 요령은 모든 로직을 Python으로 작성하고 이를 cffi 콜백을 통해 노출하는 것입니다. 간단한 C 프로그램을 작성해 봅시다:

#include "PyPy.h"
#include <stdio.h>
#include <stdlib.h>

static char source[] = "print 'hello from pypy'";

int main(void)
{
    int res;

    rpython_startup_code();
    /* Before PyPy 5.5, you may need to say e.g. "/opt/pypy/bin" instead
     * of NULL. */
    res = pypy_setup_home(NULL, 1);
    if (res) {
        printf("Error setting pypy home!\n");
        return 1;
    }

    res = pypy_execute_source((char*)source);
    if (res) {
        printf("Error calling pypy_execute_source!\n");
    }
    return res;
}

이제 이것을 x.c로 저장하고 컴파일한 다음, (리눅스에서) 다음 명령으로 실행하면:

$ gcc -g -o x x.c -lpypy-c -L/opt/pypy/bin -I/opt/pypy/include
$ LD_LIBRARY_PATH=/opt/pypy/bin ./x
hello from pypy

OSX에서 바이너리에 링크하려면 다음과 같은 명령으로 해당 바이너리의 rpath를 설정해야 합니다.:

gcc -o x x.c -lpypy-c -L. -Wl,-rpath -Wl,@executable_path
./x
hello from pypy

더 완전한 예제

Note

extern "Python"을 사용하지 않는다는 점에 유의하십시오. 새로운 CFFI 1.4에서 콜백을 처리하는 방식입니다. 이는 이 예제들이 API 모드가 아닌 ABI 모드를 사용하기 때문이며, ABI 모드에서는 여전히 ffi.callback()을 사용해야 합니다. extern "Python"을 임베딩 개념과 통합하는 작업이 진행 중입니다(이 작업은 궁극적으로 여기에서 설명한 방식보다 더 나은 임베딩 방식으로 이어질 것으로 예상되며, 그 방식은 CPython과 PyPy에서 똑같이 잘 동작할 것입니다).

보통은 단순히 소스를 실행하는 것 이상의 작업이 필요합니다. 다음은 완전한 형태를 갖춘 예제입니다. 자세한 내용은 cffi 문서를 참고하십시오. 다소 길지만, PyPy 임베딩 인터페이스로 무엇을 할 수 있는지 그 요점을 잘 보여 줍니다:

# file "interface.py"

import cffi

ffi = cffi.FFI()
ffi.cdef('''
struct API {
    double (*add_numbers)(double x, double y);
};
''')

# Better define callbacks at module scope, it's important to
# keep this object alive.
@ffi.callback("double (double, double)")
def add_numbers(x, y):
    return x + y

def fill_api(ptr):
    global api
    api = ffi.cast("struct API*", ptr)
    api.add_numbers = add_numbers
/* C example */
#include "PyPy.h"
#include <stdio.h>
#include <stdlib.h>

struct API {
    double (*add_numbers)(double x, double y);
};

struct API api;   /* global var */

int initialize_api(void)
{
    static char source[] =
        "import sys; sys.path.insert(0, '.'); "
        "import interface; interface.fill_api(c_argument)";
    int res;

    rpython_startup_code();
    res = pypy_setup_home(NULL, 1);
    if (res) {
        fprintf(stderr, "Error setting pypy home!\n");
        return -1;
    }
    res = pypy_execute_source_ptr(source, &api);
    if (res) {
        fprintf(stderr, "Error calling pypy_execute_source_ptr!\n");
        return -1;
    }
    return 0;
}

int main(void)
{
    if (initialize_api() < 0)
        return 1;

    printf("sum: %f\n", api.add_numbers(12.3, 45.6));

    return 0;
}

다음과 같이 컴파일하고 실행할 수 있습니다.:

$ gcc -g -o x x.c -lpypy-c -L/opt/pypy/bin -I/opt/pypy/include
$ LD_LIBRARY_PATH=/opt/pypy/bin ./x
sum: 57.900000

보시다시피, 우리가 한 일은 이 특정한 경우에 필요한 사용자 정의 API를 담은 struct API를 만든 것입니다. 이 구조체는 Python이 채워서 함수 포인터를 담게 되며, 이 함수 포인터는 이후 C 쪽에서 호출됩니다. C 쪽에서 채우고 Python 쪽에서 호출하는 다른 함수 포인터를 두거나, 심지어 함수 포인터가 아닌 필드를 두는 것도 가능합니다. 기본적으로 양쪽은 여러분의 API를 정의하는 이 단일 C 구조체를 통해 통신합니다.

pypy_home 찾기

2016년 10월에 출시된 PyPy >= 5.5를 실행하고 있다면 대개 이 절은 건너뛰어도 됩니다

pypy_setup_home() 함수는 첫 번째 매개변수로 파일 경로를 받으며, 이 파일로부터 표준 라이브러리의 위치를 추론합니다. 더 정확히 말하면, lib-python과 lib_pypy를 찾을 때까지 경로의 마지막 구성 요소를 하나씩 제거해 나갑니다. 현재로서는 이 경로를 찾는 “깔끔한” 방법(pkg-config 같은 것이 떠오릅니다)이 없습니다. 다음과 같은 (GNU 전용) 편법을 시도해 볼 수 있습니다(dl을 링크하는 것을 잊지 마십시오). 이 방법은 libpypy-c.so가 표준 라이브러리 디렉터리 안에 있다고 가정합니다. (어차피 대체로 그래야만 합니다. 그렇지 않으면 pypy 프로그램 자체가 실행되지 않습니다.)

#if !(_GNU_SOURCE)
#define _GNU_SOURCE
#endif

#include <dlfcn.h>
#include <limits.h>
#include <stdlib.h>

// caller should free returned pointer to avoid memleaks
// returns NULL on error
char* guess_pypyhome(void) {
    // glibc-only (dladdr is why we #define _GNU_SOURCE)
    Dl_info info;
    void *_rpython_startup_code = dlsym(0,"rpython_startup_code");
    if (_rpython_startup_code == 0) {
        return 0;
    }
    if (dladdr(_rpython_startup_code, &info) != 0) {
        const char* lib_path = info.dli_fname;
        char* lib_realpath = realpath(lib_path, 0);
        return lib_realpath;
    }
    return 0;
}

스레딩

pthreads를 사용하려면 직접 생성한 각 스레드에서(단, 메인 스레드는 제외) pypy_thread_attach를 호출하고, 메인 스레드에서 pypy_init_threads를 호출해야 합니다.