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를 호출해야 합니다.