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부터는 CPython과 PyPy 모두에서 작동하는 더 간단한 접근 방식을 제공하는 CFFI의 native embedding support,를 대신 사용하는 것이 권장됩니다.

결과로 생성된 공유 라이브러리는 매우 적은 수의 함수만 노출합니다. 이들은 v7.3.8에서 제거되었으나 is still availablePyPy.h에 정의되어 있습니다. 몇 가지 원칙만 따른다면, 필요한 모든 작업을 수행하기에 충분합니다. API는 다음과 같습니다:

void rpython_startup_code(void);

이것은 다른 무언가를 호출하기 전에 (한 번) 호출해야 하는 함수입니다. 이 함수는 RPython/PyPy 가비지 컬렉터를 초기화하고 필요한 여러 시작 코드를 수행합니다. 이 함수는 실패할 수 없습니다.

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 이상에서는 매 호출마다 새로운 전역(globals) 딕셔너리를 얻습니다(다만 이 경우 모든 전역 딕셔너리는 sys._pypy_execute_source에서 영원히 살아 있는 상태로 유지됩니다).

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

Note

2014년 6월 PyPy 2.3.1에서 추가됨

위와 마찬가지이지만, 소스 스코프에 void*가 Python int로 인코딩되는 c_argument라는 매직 인자를 등록한다는 점이 다릅니다.

void pypy_thread_attach(void);

PyPy 외부에서 초기화된 스레드를 사용하는 애플리케이션의 경우, 이 스레드를 추적하도록 PyPy GC에 알리기 위해 이 함수를 호출해야 합니다. 이 함수 자체는 스레드 안전하지 않으므로, 뮤텍스 가드(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

여기서는 CFFI 1.4에서 콜백을 수행하는 새로운 방법인 extern "Python"을 사용하지 않는다는 점에 유의하십시오. 이는 이 예제들이 API 모드가 아닌 ABI 모드를 사용하기 때문이며, ABI 모드에서는 여전히 ffi.callback()을 사용해야 합니다. extern "Python"을 임베딩(embedding)이라는 개념과 통합하는 작업이 진행 중입니다(그리고 이는 궁극적으로 여기서 설명한 것보다 더 나은 임베딩 방법으로 이어질 것으로 예상되며, 이는 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 찾기

PyPy >= 5.5(2016년 10월 출시)를 실행 중이라면 보통 이 절을 건너뛰어도 됩니다

함수 pypy_setup_home()은 첫 번째 매개변수로 표준 라이브러리의 위치를 추측할 수 있는 파일의 경로를 받습니다. 더 정확히 말하면, lib-pythonlib_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를 호출해야 합니다.