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