Current section

Files

Jump to
erlang_python c_src py_wsgi.c
Raw

c_src/py_wsgi.c

/*
* Copyright 2026 Benoit Chesneau
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @file py_wsgi.c
* @brief WSGI marshalling optimizations implementation
* @author Benoit Chesneau
*
* This file implements optimized WSGI request/response handling:
* - Interned Python string keys for environ dicts
* - Pre-built constant values reused across requests
* - Direct WSGI NIF for maximum performance
*
* @note This file is included from py_nif.c (single compilation unit)
*/
/* ============================================================================
* Per-Interpreter State Management
* ============================================================================ */
/* Storage for per-interpreter states */
static wsgi_interp_state_t *g_wsgi_interp_states[WSGI_MAX_INTERPRETERS];
static int g_wsgi_interp_state_count = 0;
static pthread_mutex_t g_wsgi_interp_state_mutex = PTHREAD_MUTEX_INITIALIZER;
/* Flag: WSGI subsystem is initialized (not per-interpreter) */
static bool g_wsgi_initialized = false;
/**
* @brief Initialize a single interpreter state for WSGI
*/
static int init_wsgi_interp_state(wsgi_interp_state_t *state) {
/* CGI/1.1 environ keys */
state->key_request_method = PyUnicode_InternFromString("REQUEST_METHOD");
if (!state->key_request_method) return -1;
state->key_script_name = PyUnicode_InternFromString("SCRIPT_NAME");
if (!state->key_script_name) return -1;
state->key_path_info = PyUnicode_InternFromString("PATH_INFO");
if (!state->key_path_info) return -1;
state->key_query_string = PyUnicode_InternFromString("QUERY_STRING");
if (!state->key_query_string) return -1;
state->key_content_type = PyUnicode_InternFromString("CONTENT_TYPE");
if (!state->key_content_type) return -1;
state->key_content_length = PyUnicode_InternFromString("CONTENT_LENGTH");
if (!state->key_content_length) return -1;
state->key_server_name = PyUnicode_InternFromString("SERVER_NAME");
if (!state->key_server_name) return -1;
state->key_server_port = PyUnicode_InternFromString("SERVER_PORT");
if (!state->key_server_port) return -1;
state->key_server_protocol = PyUnicode_InternFromString("SERVER_PROTOCOL");
if (!state->key_server_protocol) return -1;
state->key_remote_host = PyUnicode_InternFromString("REMOTE_HOST");
if (!state->key_remote_host) return -1;
state->key_remote_addr = PyUnicode_InternFromString("REMOTE_ADDR");
if (!state->key_remote_addr) return -1;
/* WSGI environ keys */
state->key_wsgi_version = PyUnicode_InternFromString("wsgi.version");
if (!state->key_wsgi_version) return -1;
state->key_wsgi_url_scheme = PyUnicode_InternFromString("wsgi.url_scheme");
if (!state->key_wsgi_url_scheme) return -1;
state->key_wsgi_input = PyUnicode_InternFromString("wsgi.input");
if (!state->key_wsgi_input) return -1;
state->key_wsgi_errors = PyUnicode_InternFromString("wsgi.errors");
if (!state->key_wsgi_errors) return -1;
state->key_wsgi_multithread = PyUnicode_InternFromString("wsgi.multithread");
if (!state->key_wsgi_multithread) return -1;
state->key_wsgi_multiprocess = PyUnicode_InternFromString("wsgi.multiprocess");
if (!state->key_wsgi_multiprocess) return -1;
state->key_wsgi_run_once = PyUnicode_InternFromString("wsgi.run_once");
if (!state->key_wsgi_run_once) return -1;
/* WSGI extensions */
state->key_wsgi_file_wrapper = PyUnicode_InternFromString("wsgi.file_wrapper");
if (!state->key_wsgi_file_wrapper) return -1;
state->key_wsgi_input_terminated = PyUnicode_InternFromString("wsgi.input_terminated");
if (!state->key_wsgi_input_terminated) return -1;
state->key_wsgi_early_hints = PyUnicode_InternFromString("wsgi.early_hints");
if (!state->key_wsgi_early_hints) return -1;
/* HTTP protocol constants */
state->http_10 = PyUnicode_InternFromString("HTTP/1.0");
if (!state->http_10) return -1;
state->http_11 = PyUnicode_InternFromString("HTTP/1.1");
if (!state->http_11) return -1;
/* Scheme constants */
state->scheme_http = PyUnicode_InternFromString("http");
if (!state->scheme_http) return -1;
state->scheme_https = PyUnicode_InternFromString("https");
if (!state->scheme_https) return -1;
/* HTTP methods */
state->method_get = PyUnicode_InternFromString("GET");
if (!state->method_get) return -1;
state->method_post = PyUnicode_InternFromString("POST");
if (!state->method_post) return -1;
state->method_put = PyUnicode_InternFromString("PUT");
if (!state->method_put) return -1;
state->method_delete = PyUnicode_InternFromString("DELETE");
if (!state->method_delete) return -1;
state->method_head = PyUnicode_InternFromString("HEAD");
if (!state->method_head) return -1;
state->method_options = PyUnicode_InternFromString("OPTIONS");
if (!state->method_options) return -1;
state->method_patch = PyUnicode_InternFromString("PATCH");
if (!state->method_patch) return -1;
/* WSGI version tuple (1, 0) */
state->wsgi_version_tuple = PyTuple_Pack(2, PyLong_FromLong(1), PyLong_FromLong(0));
if (!state->wsgi_version_tuple) return -1;
/* Boolean constants */
state->py_true = Py_True;
Py_INCREF(state->py_true);
state->py_false = Py_False;
Py_INCREF(state->py_false);
/* Empty values */
state->empty_string = PyUnicode_InternFromString("");
if (!state->empty_string) return -1;
state->empty_bytes = PyBytes_FromStringAndSize("", 0);
if (!state->empty_bytes) return -1;
/* Import io.BytesIO for wsgi.input */
PyObject *io_module = PyImport_ImportModule("io");
if (!io_module) return -1;
state->bytesio_class = PyObject_GetAttrString(io_module, "BytesIO");
Py_DECREF(io_module);
if (!state->bytesio_class) return -1;
/* HTTP header prefix */
state->http_prefix = PyUnicode_InternFromString("HTTP_");
if (!state->http_prefix) return -1;
state->initialized = true;
return 0;
}
/**
* @brief Clean up a single interpreter state for WSGI
*/
static void cleanup_wsgi_interp_state_internal(wsgi_interp_state_t *state) {
if (!state || !state->initialized) return;
Py_XDECREF(state->key_request_method);
Py_XDECREF(state->key_script_name);
Py_XDECREF(state->key_path_info);
Py_XDECREF(state->key_query_string);
Py_XDECREF(state->key_content_type);
Py_XDECREF(state->key_content_length);
Py_XDECREF(state->key_server_name);
Py_XDECREF(state->key_server_port);
Py_XDECREF(state->key_server_protocol);
Py_XDECREF(state->key_remote_host);
Py_XDECREF(state->key_remote_addr);
Py_XDECREF(state->key_wsgi_version);
Py_XDECREF(state->key_wsgi_url_scheme);
Py_XDECREF(state->key_wsgi_input);
Py_XDECREF(state->key_wsgi_errors);
Py_XDECREF(state->key_wsgi_multithread);
Py_XDECREF(state->key_wsgi_multiprocess);
Py_XDECREF(state->key_wsgi_run_once);
Py_XDECREF(state->key_wsgi_file_wrapper);
Py_XDECREF(state->key_wsgi_input_terminated);
Py_XDECREF(state->key_wsgi_early_hints);
Py_XDECREF(state->http_10);
Py_XDECREF(state->http_11);
Py_XDECREF(state->scheme_http);
Py_XDECREF(state->scheme_https);
Py_XDECREF(state->method_get);
Py_XDECREF(state->method_post);
Py_XDECREF(state->method_put);
Py_XDECREF(state->method_delete);
Py_XDECREF(state->method_head);
Py_XDECREF(state->method_options);
Py_XDECREF(state->method_patch);
Py_XDECREF(state->wsgi_version_tuple);
Py_XDECREF(state->py_true);
Py_XDECREF(state->py_false);
Py_XDECREF(state->empty_string);
Py_XDECREF(state->empty_bytes);
Py_XDECREF(state->bytesio_class);
Py_XDECREF(state->http_prefix);
/* Clean up callable cache */
if (state->cached_module_name) {
enif_free(state->cached_module_name);
state->cached_module_name = NULL;
}
if (state->cached_callable_name) {
enif_free(state->cached_callable_name);
state->cached_callable_name = NULL;
}
Py_XDECREF(state->cached_callable);
state->cached_callable = NULL;
if (state->cached_runner_name) {
enif_free(state->cached_runner_name);
state->cached_runner_name = NULL;
}
Py_XDECREF(state->cached_runner);
state->cached_runner = NULL;
Py_XDECREF(state->cached_run_func);
state->cached_run_func = NULL;
state->initialized = false;
}
/**
* @brief Get or create per-interpreter state for WSGI
*/
wsgi_interp_state_t *get_wsgi_interp_state(void) {
PyInterpreterState *interp = PyInterpreterState_Get();
/* Fast path: check existing states without lock */
for (int i = 0; i < g_wsgi_interp_state_count; i++) {
if (g_wsgi_interp_states[i] && g_wsgi_interp_states[i]->interp == interp) {
return g_wsgi_interp_states[i];
}
}
/* Slow path: acquire lock and create new state */
pthread_mutex_lock(&g_wsgi_interp_state_mutex);
/* Double-check after acquiring lock */
for (int i = 0; i < g_wsgi_interp_state_count; i++) {
if (g_wsgi_interp_states[i] && g_wsgi_interp_states[i]->interp == interp) {
pthread_mutex_unlock(&g_wsgi_interp_state_mutex);
return g_wsgi_interp_states[i];
}
}
/* Check capacity */
if (g_wsgi_interp_state_count >= WSGI_MAX_INTERPRETERS) {
pthread_mutex_unlock(&g_wsgi_interp_state_mutex);
PyErr_SetString(PyExc_RuntimeError, "Too many Python interpreters for WSGI");
return NULL;
}
/* Allocate and initialize new state */
wsgi_interp_state_t *state = enif_alloc(sizeof(wsgi_interp_state_t));
if (!state) {
pthread_mutex_unlock(&g_wsgi_interp_state_mutex);
PyErr_NoMemory();
return NULL;
}
memset(state, 0, sizeof(wsgi_interp_state_t));
state->interp = interp;
if (init_wsgi_interp_state(state) < 0) {
cleanup_wsgi_interp_state_internal(state);
enif_free(state);
pthread_mutex_unlock(&g_wsgi_interp_state_mutex);
return NULL;
}
g_wsgi_interp_states[g_wsgi_interp_state_count++] = state;
pthread_mutex_unlock(&g_wsgi_interp_state_mutex);
return state;
}
/**
* @brief Clean up state for a specific interpreter
*/
void cleanup_wsgi_interp_state(PyInterpreterState *interp) {
if (!interp) {
interp = PyInterpreterState_Get();
}
pthread_mutex_lock(&g_wsgi_interp_state_mutex);
for (int i = 0; i < g_wsgi_interp_state_count; i++) {
if (g_wsgi_interp_states[i] && g_wsgi_interp_states[i]->interp == interp) {
cleanup_wsgi_interp_state_internal(g_wsgi_interp_states[i]);
enif_free(g_wsgi_interp_states[i]);
/* Shift remaining states down */
for (int j = i; j < g_wsgi_interp_state_count - 1; j++) {
g_wsgi_interp_states[j] = g_wsgi_interp_states[j + 1];
}
g_wsgi_interp_states[--g_wsgi_interp_state_count] = NULL;
break;
}
}
pthread_mutex_unlock(&g_wsgi_interp_state_mutex);
}
/**
* @brief Clean up all interpreter states
*/
void cleanup_all_wsgi_interp_states(void) {
pthread_mutex_lock(&g_wsgi_interp_state_mutex);
for (int i = 0; i < g_wsgi_interp_state_count; i++) {
if (g_wsgi_interp_states[i]) {
cleanup_wsgi_interp_state_internal(g_wsgi_interp_states[i]);
enif_free(g_wsgi_interp_states[i]);
g_wsgi_interp_states[i] = NULL;
}
}
g_wsgi_interp_state_count = 0;
pthread_mutex_unlock(&g_wsgi_interp_state_mutex);
}
/* ============================================================================
* Callable Caching
* ============================================================================
* Cache module imports and callable lookups to avoid per-request overhead.
* Most WSGI apps use a single module/callable, so a simple last-used cache
* provides excellent hit rates.
*/
/**
* @brief Get cached runner function or import and cache it
*
* Returns a borrowed reference to the cached _run_wsgi_sync function.
*/
static PyObject *get_cached_wsgi_runner(wsgi_interp_state_t *state,
const char *runner_name) {
/* Check if cached */
if (state->cached_runner_name &&
strcmp(state->cached_runner_name, runner_name) == 0 &&
state->cached_run_func != NULL) {
return state->cached_run_func; /* Cache hit - borrowed reference */
}
/* Cache miss - import and cache */
PyObject *runner = PyImport_ImportModule(runner_name);
if (!runner) return NULL;
PyObject *run_func = PyObject_GetAttrString(runner, "_run_wsgi_sync");
if (!run_func) {
Py_DECREF(runner);
return NULL;
}
/* Update cache */
if (state->cached_runner_name) {
enif_free(state->cached_runner_name);
}
Py_XDECREF(state->cached_runner);
Py_XDECREF(state->cached_run_func);
state->cached_runner_name = enif_alloc(strlen(runner_name) + 1);
if (!state->cached_runner_name) {
Py_DECREF(run_func);
Py_DECREF(runner);
return NULL;
}
strcpy(state->cached_runner_name, runner_name);
state->cached_runner = runner; /* Takes ownership */
state->cached_run_func = run_func; /* Takes ownership */
return run_func; /* Borrowed reference */
}
/* ============================================================================
* Initialization / Cleanup
* ============================================================================ */
static int wsgi_scope_init(void) {
if (g_wsgi_initialized) {
return 0;
}
wsgi_interp_state_t *state = get_wsgi_interp_state();
if (!state) {
return -1;
}
g_wsgi_initialized = true;
return 0;
}
static void wsgi_scope_cleanup(void) {
if (!g_wsgi_initialized) {
return;
}
cleanup_all_wsgi_interp_states();
g_wsgi_initialized = false;
}
/* ============================================================================
* Helper Functions
* ============================================================================ */
static PyObject *wsgi_get_method(const char *method, size_t len) {
/* Check common methods - return new reference */
switch (len) {
case 3:
if (memcmp(method, "GET", 3) == 0) {
Py_INCREF(WSGI_METHOD_GET);
return WSGI_METHOD_GET;
}
if (memcmp(method, "PUT", 3) == 0) {
Py_INCREF(WSGI_METHOD_PUT);
return WSGI_METHOD_PUT;
}
break;
case 4:
if (memcmp(method, "POST", 4) == 0) {
Py_INCREF(WSGI_METHOD_POST);
return WSGI_METHOD_POST;
}
if (memcmp(method, "HEAD", 4) == 0) {
Py_INCREF(WSGI_METHOD_HEAD);
return WSGI_METHOD_HEAD;
}
break;
case 5:
if (memcmp(method, "PATCH", 5) == 0) {
Py_INCREF(WSGI_METHOD_PATCH);
return WSGI_METHOD_PATCH;
}
break;
case 6:
if (memcmp(method, "DELETE", 6) == 0) {
Py_INCREF(WSGI_METHOD_DELETE);
return WSGI_METHOD_DELETE;
}
break;
case 7:
if (memcmp(method, "OPTIONS", 7) == 0) {
Py_INCREF(WSGI_METHOD_OPTIONS);
return WSGI_METHOD_OPTIONS;
}
break;
}
/* Uncommon method - create new string */
return PyUnicode_FromStringAndSize(method, len);
}
/**
* @brief Get cached key for an environ key term
*/
static PyObject *wsgi_get_key_for_term(ErlNifEnv *env, ERL_NIF_TERM term) {
char key_buf[128];
ErlNifBinary key_bin;
if (enif_is_atom(env, term)) {
if (enif_get_atom(env, term, key_buf, sizeof(key_buf), ERL_NIF_LATIN1)) {
/* Map to cached keys */
if (strcmp(key_buf, "REQUEST_METHOD") == 0) return WSGI_KEY_REQUEST_METHOD;
if (strcmp(key_buf, "SCRIPT_NAME") == 0) return WSGI_KEY_SCRIPT_NAME;
if (strcmp(key_buf, "PATH_INFO") == 0) return WSGI_KEY_PATH_INFO;
if (strcmp(key_buf, "QUERY_STRING") == 0) return WSGI_KEY_QUERY_STRING;
if (strcmp(key_buf, "CONTENT_TYPE") == 0) return WSGI_KEY_CONTENT_TYPE;
if (strcmp(key_buf, "CONTENT_LENGTH") == 0) return WSGI_KEY_CONTENT_LENGTH;
if (strcmp(key_buf, "SERVER_NAME") == 0) return WSGI_KEY_SERVER_NAME;
if (strcmp(key_buf, "SERVER_PORT") == 0) return WSGI_KEY_SERVER_PORT;
if (strcmp(key_buf, "SERVER_PROTOCOL") == 0) return WSGI_KEY_SERVER_PROTOCOL;
if (strcmp(key_buf, "REMOTE_HOST") == 0) return WSGI_KEY_REMOTE_HOST;
if (strcmp(key_buf, "REMOTE_ADDR") == 0) return WSGI_KEY_REMOTE_ADDR;
}
} else if (enif_inspect_binary(env, term, &key_bin)) {
/* Check for binary keys */
if (key_bin.size < sizeof(key_buf)) {
memcpy(key_buf, key_bin.data, key_bin.size);
key_buf[key_bin.size] = '\0';
/* Check common keys */
if (strcmp(key_buf, "REQUEST_METHOD") == 0) return WSGI_KEY_REQUEST_METHOD;
if (strcmp(key_buf, "SCRIPT_NAME") == 0) return WSGI_KEY_SCRIPT_NAME;
if (strcmp(key_buf, "PATH_INFO") == 0) return WSGI_KEY_PATH_INFO;
if (strcmp(key_buf, "QUERY_STRING") == 0) return WSGI_KEY_QUERY_STRING;
if (strcmp(key_buf, "CONTENT_TYPE") == 0) return WSGI_KEY_CONTENT_TYPE;
if (strcmp(key_buf, "CONTENT_LENGTH") == 0) return WSGI_KEY_CONTENT_LENGTH;
if (strcmp(key_buf, "SERVER_NAME") == 0) return WSGI_KEY_SERVER_NAME;
if (strcmp(key_buf, "SERVER_PORT") == 0) return WSGI_KEY_SERVER_PORT;
if (strcmp(key_buf, "SERVER_PROTOCOL") == 0) return WSGI_KEY_SERVER_PROTOCOL;
if (strcmp(key_buf, "REMOTE_HOST") == 0) return WSGI_KEY_REMOTE_HOST;
if (strcmp(key_buf, "REMOTE_ADDR") == 0) return WSGI_KEY_REMOTE_ADDR;
if (strcmp(key_buf, "wsgi.version") == 0) return WSGI_KEY_WSGI_VERSION;
if (strcmp(key_buf, "wsgi.url_scheme") == 0) return WSGI_KEY_WSGI_URL_SCHEME;
if (strcmp(key_buf, "wsgi.input") == 0) return WSGI_KEY_WSGI_INPUT;
if (strcmp(key_buf, "wsgi.errors") == 0) return WSGI_KEY_WSGI_ERRORS;
if (strcmp(key_buf, "wsgi.multithread") == 0) return WSGI_KEY_WSGI_MULTITHREAD;
if (strcmp(key_buf, "wsgi.multiprocess") == 0) return WSGI_KEY_WSGI_MULTIPROCESS;
if (strcmp(key_buf, "wsgi.run_once") == 0) return WSGI_KEY_WSGI_RUN_ONCE;
}
}
return NULL;
}
/**
* @brief Convert WSGI environ map from Erlang to Python dict
*/
static PyObject *wsgi_environ_from_map(ErlNifEnv *env, ERL_NIF_TERM environ_map) {
if (!g_wsgi_initialized) {
PyErr_SetString(PyExc_RuntimeError, "WSGI cache not initialized");
return NULL;
}
if (!enif_is_map(env, environ_map)) {
PyErr_SetString(PyExc_TypeError, "expected map for WSGI environ");
return NULL;
}
PyObject *environ = PyDict_New();
if (environ == NULL) {
return NULL;
}
ERL_NIF_TERM key, value;
ErlNifMapIterator iter;
enif_map_iterator_create(env, environ_map, &iter, ERL_NIF_MAP_ITERATOR_FIRST);
while (enif_map_iterator_get_pair(env, &iter, &key, &value)) {
PyObject *py_key = wsgi_get_key_for_term(env, key);
bool key_borrowed = (py_key != NULL);
if (py_key == NULL) {
/* Unknown key - create new Python string */
py_key = term_to_py(env, key);
if (py_key == NULL) {
enif_map_iterator_destroy(env, &iter);
Py_DECREF(environ);
return NULL;
}
}
/* Convert value with optimizations for known keys */
PyObject *py_value = NULL;
if (py_key == WSGI_KEY_REQUEST_METHOD) {
ErlNifBinary method_bin;
if (enif_inspect_binary(env, value, &method_bin)) {
py_value = wsgi_get_method((char *)method_bin.data, method_bin.size);
}
} else if (py_key == WSGI_KEY_SERVER_PROTOCOL) {
ErlNifBinary proto_bin;
if (enif_inspect_binary(env, value, &proto_bin)) {
if (proto_bin.size == 8 && memcmp(proto_bin.data, "HTTP/1.1", 8) == 0) {
Py_INCREF(WSGI_HTTP_11);
py_value = WSGI_HTTP_11;
} else if (proto_bin.size == 8 && memcmp(proto_bin.data, "HTTP/1.0", 8) == 0) {
Py_INCREF(WSGI_HTTP_10);
py_value = WSGI_HTTP_10;
}
}
} else if (py_key == WSGI_KEY_WSGI_URL_SCHEME) {
ErlNifBinary scheme_bin;
if (enif_inspect_binary(env, value, &scheme_bin)) {
if (scheme_bin.size == 4 && memcmp(scheme_bin.data, "http", 4) == 0) {
Py_INCREF(WSGI_SCHEME_HTTP);
py_value = WSGI_SCHEME_HTTP;
} else if (scheme_bin.size == 5 && memcmp(scheme_bin.data, "https", 5) == 0) {
Py_INCREF(WSGI_SCHEME_HTTPS);
py_value = WSGI_SCHEME_HTTPS;
}
}
} else if (py_key == WSGI_KEY_SCRIPT_NAME || py_key == WSGI_KEY_PATH_INFO ||
py_key == WSGI_KEY_QUERY_STRING) {
ErlNifBinary str_bin;
if (enif_inspect_binary(env, value, &str_bin) && str_bin.size == 0) {
Py_INCREF(WSGI_EMPTY_STRING);
py_value = WSGI_EMPTY_STRING;
}
} else if (py_key == WSGI_KEY_WSGI_VERSION) {
/* Always use cached version tuple */
Py_INCREF(WSGI_VERSION_TUPLE);
py_value = WSGI_VERSION_TUPLE;
} else if (py_key == WSGI_KEY_WSGI_MULTITHREAD ||
py_key == WSGI_KEY_WSGI_MULTIPROCESS) {
/* These are typically true */
if (enif_is_atom(env, value)) {
char atom_buf[16];
if (enif_get_atom(env, value, atom_buf, sizeof(atom_buf), ERL_NIF_LATIN1)) {
if (strcmp(atom_buf, "true") == 0) {
Py_INCREF(WSGI_PY_TRUE);
py_value = WSGI_PY_TRUE;
} else if (strcmp(atom_buf, "false") == 0) {
Py_INCREF(WSGI_PY_FALSE);
py_value = WSGI_PY_FALSE;
}
}
}
} else if (py_key == WSGI_KEY_WSGI_RUN_ONCE) {
/* Typically false */
if (enif_is_atom(env, value)) {
char atom_buf[16];
if (enif_get_atom(env, value, atom_buf, sizeof(atom_buf), ERL_NIF_LATIN1)) {
if (strcmp(atom_buf, "false") == 0) {
Py_INCREF(WSGI_PY_FALSE);
py_value = WSGI_PY_FALSE;
} else if (strcmp(atom_buf, "true") == 0) {
Py_INCREF(WSGI_PY_TRUE);
py_value = WSGI_PY_TRUE;
}
}
}
} else if (py_key == WSGI_KEY_WSGI_INPUT) {
/* Create BytesIO from binary body */
ErlNifBinary body_bin;
if (enif_inspect_binary(env, value, &body_bin)) {
PyObject *body_bytes = PyBytes_FromStringAndSize(
(char *)body_bin.data, body_bin.size);
if (body_bytes) {
py_value = PyObject_CallFunctionObjArgs(
WSGI_BYTESIO_CLASS, body_bytes, NULL);
Py_DECREF(body_bytes);
}
}
}
/* Generic conversion if no optimization applied */
if (py_value == NULL) {
py_value = term_to_py(env, value);
if (py_value == NULL) {
if (!key_borrowed) {
Py_DECREF(py_key);
}
enif_map_iterator_destroy(env, &iter);
Py_DECREF(environ);
return NULL;
}
}
/* Use interned key reference */
if (key_borrowed) {
Py_INCREF(py_key);
}
int set_result = PyDict_SetItem(environ, py_key, py_value);
Py_DECREF(py_key);
Py_DECREF(py_value);
if (set_result < 0) {
enif_map_iterator_destroy(env, &iter);
Py_DECREF(environ);
return NULL;
}
enif_map_iterator_next(env, &iter);
}
enif_map_iterator_destroy(env, &iter);
return environ;
}
/* ============================================================================
* NIF Function
* ============================================================================ */
static ERL_NIF_TERM nif_wsgi_run(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
if (argc < 4) {
return make_error(env, "badarg");
}
if (!runtime_is_running()) {
return make_error(env, "python_not_initialized");
}
if (!g_wsgi_initialized) {
/* Initialize lazily */
PyGILState_STATE init_gstate = PyGILState_Ensure();
int init_result = wsgi_scope_init();
PyGILState_Release(init_gstate);
if (init_result < 0) {
return make_error(env, "wsgi_init_failed");
}
}
ErlNifBinary runner_bin, module_bin, callable_bin;
if (!enif_inspect_binary(env, argv[0], &runner_bin)) {
return make_error(env, "invalid_runner");
}
if (!enif_inspect_binary(env, argv[1], &module_bin)) {
return make_error(env, "invalid_module");
}
if (!enif_inspect_binary(env, argv[2], &callable_bin)) {
return make_error(env, "invalid_callable");
}
PyGILState_STATE gstate = PyGILState_Ensure();
ERL_NIF_TERM result;
/* Convert names */
char *runner_name = binary_to_string(&runner_bin);
char *module_name = binary_to_string(&module_bin);
char *callable_name = binary_to_string(&callable_bin);
if (runner_name == NULL || module_name == NULL || callable_name == NULL) {
enif_free(runner_name);
enif_free(module_name);
enif_free(callable_name);
PyGILState_Release(gstate);
return make_error(env, "alloc_failed");
}
/* Get per-interpreter state for callable caching */
wsgi_interp_state_t *state = get_wsgi_interp_state();
if (state == NULL) {
result = make_py_error(env);
goto cleanup;
}
/* Build optimized environ dict from Erlang map */
PyObject *environ = wsgi_environ_from_map(env, argv[3]);
if (environ == NULL) {
result = make_py_error(env);
goto cleanup;
}
/* Get cached runner function (avoids per-request import) */
PyObject *run_func = get_cached_wsgi_runner(state, runner_name);
if (run_func == NULL) {
Py_DECREF(environ);
result = make_py_error(env);
goto cleanup;
}
/* Create Python strings for module/callable names (for runner call) */
PyObject *py_module_name = PyUnicode_FromString(module_name);
PyObject *py_callable_name = PyUnicode_FromString(callable_name);
if (py_module_name == NULL || py_callable_name == NULL) {
Py_XDECREF(py_module_name);
Py_XDECREF(py_callable_name);
Py_DECREF(environ);
result = make_py_error(env);
goto cleanup;
}
/* Call _run_wsgi_sync(module_name, callable_name, environ) using cached function */
PyObject *run_result = PyObject_CallFunctionObjArgs(
run_func, py_module_name, py_callable_name, environ, NULL);
Py_DECREF(py_module_name);
Py_DECREF(py_callable_name);
Py_DECREF(environ);
/* Note: run_func is borrowed reference from cache - don't decref */
if (run_result == NULL) {
result = make_py_error(env);
goto cleanup;
}
/* Convert result to Erlang term */
ERL_NIF_TERM term_result = py_to_term(env, run_result);
Py_DECREF(run_result);
result = enif_make_tuple2(env, ATOM_OK, term_result);
cleanup:
enif_free(runner_name);
enif_free(module_name);
enif_free(callable_name);
PyGILState_Release(gstate);
return result;
}