Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions c_src/python.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ DEF_SYMBOL(PyDict_Next)
DEF_SYMBOL(PyDict_SetItem)
DEF_SYMBOL(PyDict_SetItemString)
DEF_SYMBOL(PyDict_Size)
DEF_SYMBOL(PyErr_Clear)
DEF_SYMBOL(PyErr_Fetch)
DEF_SYMBOL(PyErr_Occurred)
DEF_SYMBOL(PyEval_GetBuiltins)
Expand Down Expand Up @@ -101,6 +102,7 @@ void load_python_library(std::string path) {
LOAD_SYMBOL(python_library, PyDict_SetItem)
LOAD_SYMBOL(python_library, PyDict_SetItemString)
LOAD_SYMBOL(python_library, PyDict_Size)
LOAD_SYMBOL(python_library, PyErr_Clear)
LOAD_SYMBOL(python_library, PyErr_Fetch)
LOAD_SYMBOL(python_library, PyErr_Occurred)
LOAD_SYMBOL(python_library, PyEval_GetBuiltins)
Expand Down
1 change: 1 addition & 0 deletions c_src/python.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ extern int (*PyDict_Next)(PyObjectPtr, Py_ssize_t *, PyObjectPtr *,
extern int (*PyDict_SetItem)(PyObjectPtr, PyObjectPtr, PyObjectPtr);
extern int (*PyDict_SetItemString)(PyObjectPtr, const char *, PyObjectPtr);
extern Py_ssize_t (*PyDict_Size)(PyObjectPtr);
extern void (*PyErr_Clear)();
extern void (*PyErr_Fetch)(PyObjectPtr *, PyObjectPtr *, PyObjectPtr *);
extern PyObjectPtr (*PyErr_Occurred)();
extern PyObjectPtr (*PyEval_GetBuiltins)();
Expand Down
275 changes: 196 additions & 79 deletions c_src/pythonx.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include <string>
#include <thread>
#include <tuple>
#include <variant>

#include "python.hpp"

Expand Down Expand Up @@ -138,15 +139,14 @@ auto ElixirPythonxJanitor = fine::Atom("Elixir.Pythonx.Janitor");
auto ElixirPythonxObject = fine::Atom("Elixir.Pythonx.Object");
auto decref = fine::Atom("decref");
auto integer = fine::Atom("integer");
auto lines = fine::Atom("lines");
auto list = fine::Atom("list");
auto map = fine::Atom("map");
auto map_set = fine::Atom("map_set");
auto output = fine::Atom("output");
auto remote_info = fine::Atom("remote_info");
auto resource = fine::Atom("resource");
auto traceback = fine::Atom("traceback");
auto tuple = fine::Atom("tuple");
auto type = fine::Atom("type");
auto value = fine::Atom("value");
} // namespace atoms

struct PyObjectResource {
Expand Down Expand Up @@ -186,8 +186,26 @@ struct PyObjectResource {

FINE_RESOURCE(PyObjectResource);

// A resource that notifies the given process upon garbage collection.
struct GCNotifier {
ErlNifPid pid;
ErlNifEnv *message_env;
ERL_NIF_TERM message_term;

GCNotifier(ErlNifPid pid, ErlNifEnv *message_env, ERL_NIF_TERM message_term)
: pid(pid), message_env(message_env), message_term(message_term) {}

void destructor(ErlNifEnv *env) {
enif_send(env, &pid, message_env, message_term);
enif_free_env(message_env);
}
};

FINE_RESOURCE(GCNotifier);

struct ExObject {
fine::ResourcePtr<PyObjectResource> resource;
std::optional<fine::Term> remote_info;

ExObject() {}
ExObject(fine::ResourcePtr<PyObjectResource> resource) : resource(resource) {}
Expand All @@ -196,26 +214,21 @@ struct ExObject {

static constexpr auto fields() {
return std::make_tuple(
std::make_tuple(&ExObject::resource, &atoms::resource));
std::make_tuple(&ExObject::resource, &atoms::resource),
std::make_tuple(&ExObject::remote_info, &atoms::remote_info));
}
};

struct ExError {
ExObject type;
ExObject value;
ExObject traceback;
std::vector<fine::Term> lines;

ExError() {}
ExError(ExObject type, ExObject value, ExObject traceback)
: type(type), value(value), traceback(traceback) {}
ExError(std::vector<fine::Term> lines) : lines(lines) {}

static constexpr auto module = &atoms::ElixirPythonxError;

static constexpr auto fields() {
return std::make_tuple(
std::make_tuple(&ExError::type, &atoms::type),
std::make_tuple(&ExError::value, &atoms::value),
std::make_tuple(&ExError::traceback, &atoms::traceback));
return std::make_tuple(std::make_tuple(&ExError::lines, &atoms::lines));
}

static constexpr auto is_exception = true;
Expand All @@ -228,30 +241,91 @@ struct EvalInfo {
std::thread::id thread_id;
};

void raise_py_error(ErlNifEnv *env) {
void raise_formatting_error_if_failed(PyObjectPtr py_object) {
if (py_object == NULL) {
throw std::runtime_error("failed while formatting a python error");
}
}

void raise_formatting_error_if_failed(const char *buffer) {
if (buffer == NULL) {
throw std::runtime_error("failed while formatting a python error");
}
}

void raise_formatting_error_if_failed(Py_ssize_t size) {
if (size == -1) {
throw std::runtime_error("failed while formatting a python error");
}
}

ExError build_py_error_from_current(ErlNifEnv *env) {
PyObjectPtr py_type, py_value, py_traceback;
PyErr_Fetch(&py_type, &py_value, &py_traceback);

// If the error indicator was set, type should not be NULL, but value
// and traceback might

// and traceback might.
if (py_type == NULL) {
throw std::runtime_error(
"raise_py_error should only be called when the error indicator is set");
throw std::runtime_error("build_py_error_from_current should only be "
"called when the error indicator is set");
}

auto type = ExObject(fine::make_resource<PyObjectResource>(py_type));

// Default value and traceback to None object
// Default value and traceback to None object.
py_value = py_value == NULL ? Py_BuildValue("") : py_value;
py_traceback = py_traceback == NULL ? Py_BuildValue("") : py_traceback;

// Format the exception. Note that if anything raises an error here,
// we throw a runtime exception, instead of a Python one, otherwise
// we could go into an infinite loop.

auto py_traceback_module = PyImport_ImportModule("traceback");
raise_formatting_error_if_failed(py_traceback_module);
auto py_traceback_module_guard = PyDecRefGuard(py_traceback_module);

auto value = fine::make_resource<PyObjectResource>(
py_value == NULL ? Py_BuildValue("") : py_value);
auto format_exception =
PyObject_GetAttrString(py_traceback_module, "format_exception");
raise_formatting_error_if_failed(format_exception);
auto format_exception_guard = PyDecRefGuard(format_exception);

auto traceback = fine::make_resource<PyObjectResource>(
py_traceback == NULL ? Py_BuildValue("") : py_traceback);
auto format_exception_args = PyTuple_Pack(3, py_type, py_value, py_traceback);
raise_formatting_error_if_failed(format_exception_args);
auto format_exception_args_guard = PyDecRefGuard(format_exception_args);

auto error = ExError(type, value, traceback);
fine::raise(env, error);
auto py_lines = PyObject_Call(format_exception, format_exception_args, NULL);
raise_formatting_error_if_failed(py_lines);
auto py_lines_guard = PyDecRefGuard(py_lines);

auto size = PyList_Size(py_lines);
raise_formatting_error_if_failed(size);

auto terms = std::vector<fine::Term>();
terms.reserve(size);

for (Py_ssize_t i = 0; i < size; i++) {
auto py_line = PyList_GetItem(py_lines, i);
raise_formatting_error_if_failed(py_line);

Py_ssize_t size;
auto buffer = PyUnicode_AsUTF8AndSize(py_line, &size);
raise_formatting_error_if_failed(buffer);

// The buffer is immutable and lives as long as the Python object,
// so we create the term as a resource binary to make it zero-copy.
Py_IncRef(py_line);
auto ex_object_resource = fine::make_resource<PyObjectResource>(py_line);
auto binary_term =
fine::make_resource_binary(env, ex_object_resource, buffer, size);

terms.push_back(binary_term);
}

return ExError(std::move(terms));
}

void raise_py_error(ErlNifEnv *env) {
fine::raise(env, build_py_error_from_current(env));
}

void raise_if_failed(ErlNifEnv *env, PyObjectPtr py_object) {
Expand Down Expand Up @@ -284,6 +358,19 @@ ERL_NIF_TERM py_str_to_binary_term(ErlNifEnv *env, PyObjectPtr py_object) {
return fine::make_resource_binary(env, ex_object_resource, buffer, size);
}

ERL_NIF_TERM py_bytes_to_binary_term(ErlNifEnv *env, PyObjectPtr py_object) {
Py_ssize_t size;
char *buffer;
auto result = PyBytes_AsStringAndSize(py_object, &buffer, &size);
raise_if_failed(env, result);

// The buffer is immutable and lives as long as the Python object,
// so we create the term as a resource binary to make it zero-copy.
Py_IncRef(py_object);
auto ex_object_resource = fine::make_resource<PyObjectResource>(py_object);
return fine::make_resource_binary(env, ex_object_resource, buffer, size);
}

fine::Ok<> init(ErlNifEnv *env, std::string python_dl_path,
ErlNifBinary python_home_path,
ErlNifBinary python_executable_path,
Expand Down Expand Up @@ -785,50 +872,6 @@ ExObject object_repr(ErlNifEnv *env, ExObject ex_object) {

FINE_NIF(object_repr, ERL_NIF_DIRTY_JOB_CPU_BOUND);

fine::Term format_exception(ErlNifEnv *env, ExError error) {
ensure_initialized();
auto gil_guard = PyGILGuard();

auto py_traceback_module = PyImport_ImportModule("traceback");
raise_if_failed(env, py_traceback_module);
auto py_traceback_module_guard = PyDecRefGuard(py_traceback_module);

auto format_exception =
PyObject_GetAttrString(py_traceback_module, "format_exception");
raise_if_failed(env, format_exception);
auto format_exception_guard = PyDecRefGuard(format_exception);

auto py_type = error.type.resource->py_object;
auto py_value = error.value.resource->py_object;
auto py_traceback = error.traceback.resource->py_object;

auto format_exception_args = PyTuple_Pack(3, py_type, py_value, py_traceback);
raise_if_failed(env, format_exception_args);
auto format_exception_args_guard = PyDecRefGuard(format_exception_args);

auto py_lines = PyObject_Call(format_exception, format_exception_args, NULL);
raise_if_failed(env, py_lines);
auto py_lines_guard = PyDecRefGuard(py_lines);

auto size = PyList_Size(py_lines);
raise_if_failed(env, size);

auto terms = std::vector<ERL_NIF_TERM>();
terms.reserve(size);

for (Py_ssize_t i = 0; i < size; i++) {
auto py_line = PyList_GetItem(py_lines, i);
raise_if_failed(env, py_line);

terms.push_back(py_str_to_binary_term(env, py_line));
}

return enif_make_list_from_array(env, terms.data(),
static_cast<unsigned int>(size));
}

FINE_NIF(format_exception, ERL_NIF_DIRTY_JOB_CPU_BOUND);

fine::Term decode_once(ErlNifEnv *env, ExObject ex_object) {
ensure_initialized();
auto gil_guard = PyGILGuard();
Expand Down Expand Up @@ -987,16 +1030,7 @@ fine::Term decode_once(ErlNifEnv *env, ExObject ex_object) {
auto is_bytes = PyObject_IsInstance(py_object, py_bytes_type);
raise_if_failed(env, is_bytes);
if (is_bytes) {
Py_ssize_t size;
char *buffer;
auto result = PyBytes_AsStringAndSize(py_object, &buffer, &size);
raise_if_failed(env, result);

// The buffer is immutable and lives as long as the Python object,
// so we create the term as a resource binary to make it zero-copy.
Py_IncRef(py_object);
auto ex_object_resource = fine::make_resource<PyObjectResource>(py_object);
return fine::make_resource_binary(env, ex_object_resource, buffer, size);
return py_bytes_to_binary_term(env, py_object);
}

auto py_set_type = PyDict_GetItemString(py_builtins, "set");
Expand Down Expand Up @@ -1461,6 +1495,86 @@ eval(ErlNifEnv *env, ErlNifBinary code, std::string code_md5,

FINE_NIF(eval, ERL_NIF_DIRTY_JOB_CPU_BOUND);

std::variant<fine::Ok<fine::Term>, fine::Error<std::string, ExError>>
dump_object(ErlNifEnv *env, ExObject ex_object) {
ensure_initialized();
auto gil_guard = PyGILGuard();

std::string pickle_module_name;
PyObjectPtr py_pickle;

py_pickle = PyImport_ImportModule("cloudpickle");
if (py_pickle != NULL) {
pickle_module_name = "cloudpickle";
} else {
// If importing fails, we ignore the error and fallback to the pickle
// module.
PyErr_Clear();
py_pickle = PyImport_ImportModule("pickle");
raise_if_failed(env, py_pickle);
pickle_module_name = "pickle";
}
auto py_pickle_guard = PyDecRefGuard(py_pickle);

auto py_dumps = PyObject_GetAttrString(py_pickle, "dumps");
raise_if_failed(env, py_dumps);
auto py_dumps_guard = PyDecRefGuard(py_dumps);

auto py_dumps_args = PyTuple_Pack(1, ex_object.resource->py_object);
raise_if_failed(env, py_dumps_args);
auto py_dumps_args_guard = PyDecRefGuard(py_dumps_args);

auto py_dump_bytes = PyObject_Call(py_dumps, py_dumps_args, NULL);
if (py_dump_bytes == NULL) {
return fine::Error<std::string, ExError>(pickle_module_name,
build_py_error_from_current(env));
}
raise_if_failed(env, py_dump_bytes);
auto py_bytes_guard = PyDecRefGuard(py_dump_bytes);

return fine::Ok<fine::Term>(py_bytes_to_binary_term(env, py_dump_bytes));
}

FINE_NIF(dump_object, ERL_NIF_DIRTY_JOB_CPU_BOUND);

ExObject load_object(ErlNifEnv *env, ErlNifBinary binary) {
ensure_initialized();
auto gil_guard = PyGILGuard();

auto py_pickle = PyImport_ImportModule("pickle");
raise_if_failed(env, py_pickle);
auto py_pickle_guard = PyDecRefGuard(py_pickle);

auto py_loads = PyObject_GetAttrString(py_pickle, "loads");
raise_if_failed(env, py_loads);
auto py_loads_guard = PyDecRefGuard(py_loads);

auto py_bytes = PyBytes_FromStringAndSize(
reinterpret_cast<const char *>(binary.data), binary.size);
raise_if_failed(env, py_bytes);
auto py_bytes_guard = PyDecRefGuard(py_bytes);

auto py_loads_args = PyTuple_Pack(1, py_bytes);
raise_if_failed(env, py_loads_args);
auto py_loads_args_guard = PyDecRefGuard(py_loads_args);

auto py_object = PyObject_Call(py_loads, py_loads_args, NULL);
raise_if_failed(env, py_object);

return ExObject(fine::make_resource<PyObjectResource>(py_object));
}

FINE_NIF(load_object, ERL_NIF_DIRTY_JOB_CPU_BOUND);

fine::ResourcePtr<GCNotifier> create_gc_notifier(ErlNifEnv *env, ErlNifPid pid,
fine::Term term) {
auto message_env = enif_alloc_env();
auto message_term = enif_make_copy(message_env, term);
return fine::make_resource<GCNotifier>(pid, message_env, message_term);
}

FINE_NIF(create_gc_notifier, 0);

} // namespace pythonx

FINE_INIT("Elixir.Pythonx.NIF");
Expand Down Expand Up @@ -1505,6 +1619,9 @@ extern "C" void pythonx_handle_io_write(const char *message,
ErlNifPid janitor_pid;
if (enif_whereis_pid(caller_env, janitor_name, &janitor_pid)) {
auto device = type == 0 ? eval_info.stdout_device : eval_info.stderr_device;
// The device term is from a different env, so we copy it into
// the message env, otherwise we may run into unexpected behaviour.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which sort of unexpected behaviour?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So far it has been working fine. Once I was testing remote eval, where stdout_device and stderr_device are remote pid terms, I run into a weird issue where the message we send below would include a random term (e.g. :infinity, {}) instead of the actual pid. I then revisited the code and realised that those terms are from a different env and doing the copy first fixed the issue. It's interesting that it's only remote pid terms that revealed the issue.

device = enif_make_copy(env, device);

auto msg = fine::encode(env, std::make_tuple(pythonx::atoms::output,
std::string(message), device));
Expand Down
Loading
Loading