Skip to content

Commit 59a180f

Browse files
Merge pull request #286 from Distributive-Network/philippe/python-stacks-in-js
Philippe/python stacks in js
2 parents 67cd65d + ce0705e commit 59a180f

File tree

4 files changed

+264
-24
lines changed

4 files changed

+264
-24
lines changed

include/ExceptionType.hh

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
/**
22
* @file ExceptionType.hh
3-
* @author Tom Tang ([email protected])
3+
* @author Tom Tang ([email protected]) and Philippe Laporte ([email protected])
44
* @brief Struct for representing Python Exception objects from a corresponding JS Error object
55
* @version 0.1
66
* @date 2023-04-11
77
*
8-
* @copyright Copyright (c) 2023
8+
* @copyright Copyright (c) 2023-2024 Distributive Corp.
99
*
1010
*/
1111

@@ -24,8 +24,6 @@
2424
*/
2525
struct ExceptionType : public PyType {
2626
public:
27-
ExceptionType(PyObject *object);
28-
2927
/**
3028
* @brief Construct a new SpiderMonkeyError from the JS Error object.
3129
*
@@ -37,11 +35,13 @@ public:
3735
const TYPE returnType = TYPE::EXCEPTION;
3836

3937
/**
40-
* @brief Convert a python [*Exception object](https://docs.python.org/3/c-api/exceptions.html#standard-exceptions) to JS Error object
38+
* @brief Convert a python Exception object to a JS Error object
4139
*
4240
* @param cx - javascript context pointer
41+
* @param exceptionValue - Exception object pointer, cannot be NULL
42+
* @param traceBack - Exception traceback pointer, can be NULL
4343
*/
44-
JSObject *toJsError(JSContext *cx);
44+
static JSObject *toJsError(JSContext *cx, PyObject *exceptionValue, PyObject *traceBack);
4545
};
4646

4747
#endif

src/ExceptionType.cc

Lines changed: 233 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
/**
2+
* @file ExceptionType.cc
3+
* @author Tom Tang ([email protected]) and Philippe Laporte ([email protected])
4+
* @brief Struct for representing Python Exception objects from a corresponding JS Error object
5+
* @date 2023-04-11
6+
*
7+
* @copyright Copyright (c) 2023-2024 Distributive Corp.
8+
*
9+
*/
10+
111
#include "include/modules/pythonmonkey/pythonmonkey.hh"
212
#include "include/setSpiderMonkeyException.hh"
313

@@ -7,8 +17,10 @@
717
#include <js/Exception.h>
818

919
#include <Python.h>
20+
#include <frameobject.h>
21+
1022

11-
ExceptionType::ExceptionType(PyObject *object) : PyType(object) {}
23+
// TODO (Tom Tang): preserve the original Python exception object somewhere in the JS obj for lossless two-way conversion
1224

1325
ExceptionType::ExceptionType(JSContext *cx, JS::HandleObject error) {
1426
// Convert the JS Error object to a Python string
@@ -26,13 +38,223 @@ ExceptionType::ExceptionType(JSContext *cx, JS::HandleObject error) {
2638
Py_XDECREF(errStr);
2739
}
2840

29-
// TODO (Tom Tang): preserve the original Python exception object somewhere in the JS obj for lossless two-way conversion
30-
JSObject *ExceptionType::toJsError(JSContext *cx) {
31-
PyObject *pyErrType = PyObject_Type(pyObject);
41+
42+
// Generating trace information
43+
44+
#define PyTraceBack_LIMIT 1000
45+
46+
static const int TB_RECURSIVE_CUTOFF = 3;
47+
48+
#if PY_VERSION_HEX >= 0x03090000
49+
50+
static inline int
51+
tb_get_lineno(PyTracebackObject *tb) {
52+
PyFrameObject *frame = tb->tb_frame;
53+
PyCodeObject *code = PyFrame_GetCode(frame);
54+
int lineno = PyCode_Addr2Line(code, tb->tb_lasti);
55+
Py_DECREF(code);
56+
return lineno;
57+
}
58+
59+
#endif
60+
61+
static int
62+
tb_print_line_repeated(_PyUnicodeWriter *writer, long cnt)
63+
{
64+
cnt -= TB_RECURSIVE_CUTOFF;
65+
PyObject *line = PyUnicode_FromFormat(
66+
(cnt > 1)
67+
? "[Previous line repeated %ld more times]\n"
68+
: "[Previous line repeated %ld more time]\n",
69+
cnt);
70+
if (line == NULL) {
71+
return -1;
72+
}
73+
int err = _PyUnicodeWriter_WriteStr(writer, line);
74+
Py_DECREF(line);
75+
return err;
76+
}
77+
78+
JSObject *ExceptionType::toJsError(JSContext *cx, PyObject *exceptionValue, PyObject *traceBack) {
79+
assert(exceptionValue != NULL);
80+
81+
PyObject *pyErrType = PyObject_Type(exceptionValue);
3282
const char *pyErrTypeName = _PyType_Name((PyTypeObject *)pyErrType);
33-
PyObject *pyErrMsg = PyObject_Str(pyObject);
34-
// TODO (Tom Tang): Convert Python traceback and set it as the `stack` property on JS Error object
35-
// PyObject *traceback = PyException_GetTraceback(pyObject);
83+
84+
PyObject *pyErrMsg = PyObject_Str(exceptionValue);
85+
86+
if (traceBack) {
87+
_PyUnicodeWriter writer;
88+
_PyUnicodeWriter_Init(&writer);
89+
90+
PyObject *fileName = NULL;
91+
int lineno = -1;
92+
93+
PyTracebackObject *tb = (PyTracebackObject *)traceBack;
94+
95+
long limit = PyTraceBack_LIMIT;
96+
97+
PyObject *limitv = PySys_GetObject("tracebacklimit");
98+
if (limitv && PyLong_Check(limitv)) {
99+
int overflow;
100+
limit = PyLong_AsLongAndOverflow(limitv, &overflow);
101+
if (overflow > 0) {
102+
limit = LONG_MAX;
103+
}
104+
else if (limit <= 0) {
105+
return NULL;
106+
}
107+
}
108+
109+
PyCodeObject *code = NULL;
110+
Py_ssize_t depth = 0;
111+
PyObject *last_file = NULL;
112+
int last_line = -1;
113+
PyObject *last_name = NULL;
114+
long cnt = 0;
115+
PyTracebackObject *tb1 = tb;
116+
int err = 0;
117+
118+
int res;
119+
PyObject *line = PyUnicode_FromString("Traceback (most recent call last):\n");
120+
if (line == NULL) {
121+
goto error;
122+
}
123+
res = _PyUnicodeWriter_WriteStr(&writer, line);
124+
Py_DECREF(line);
125+
if (res < 0) {
126+
goto error;
127+
}
128+
129+
// TODO should we reverse the stack and put it in the more common, non-python, top-most to bottom-most order? Wait for user feedback on experience
130+
while (tb1 != NULL) {
131+
depth++;
132+
tb1 = tb1->tb_next;
133+
}
134+
while (tb != NULL && depth > limit) {
135+
depth--;
136+
tb = tb->tb_next;
137+
}
138+
139+
#if PY_VERSION_HEX >= 0x03090000
140+
141+
while (tb != NULL) {
142+
code = PyFrame_GetCode(tb->tb_frame);
143+
144+
int tb_lineno = tb->tb_lineno;
145+
if (tb_lineno == -1) {
146+
tb_lineno = tb_get_lineno(tb);
147+
}
148+
149+
if (last_file == NULL ||
150+
code->co_filename != last_file ||
151+
last_line == -1 || tb_lineno != last_line ||
152+
last_name == NULL || code->co_name != last_name) {
153+
154+
if (cnt > TB_RECURSIVE_CUTOFF) {
155+
if (tb_print_line_repeated(&writer, cnt) < 0) {
156+
goto error;
157+
}
158+
}
159+
last_file = code->co_filename;
160+
last_line = tb_lineno;
161+
last_name = code->co_name;
162+
cnt = 0;
163+
}
164+
165+
cnt++;
166+
167+
if (cnt <= TB_RECURSIVE_CUTOFF) {
168+
fileName = code->co_filename;
169+
lineno = tb_lineno;
170+
171+
line = PyUnicode_FromFormat("File \"%U\", line %d, in %U\n", fileName, lineno, code->co_name);
172+
if (line == NULL) {
173+
goto error;
174+
}
175+
176+
int res = _PyUnicodeWriter_WriteStr(&writer, line);
177+
Py_DECREF(line);
178+
if (res < 0) {
179+
goto error;
180+
}
181+
}
182+
183+
Py_CLEAR(code);
184+
tb = tb->tb_next;
185+
}
186+
if (cnt > TB_RECURSIVE_CUTOFF) {
187+
if (tb_print_line_repeated(&writer, cnt) < 0) {
188+
goto error;
189+
}
190+
}
191+
192+
#else
193+
194+
while (tb != NULL && err == 0) {
195+
if (last_file == NULL ||
196+
tb->tb_frame->f_code->co_filename != last_file ||
197+
last_line == -1 || tb->tb_lineno != last_line ||
198+
last_name == NULL || tb->tb_frame->f_code->co_name != last_name) {
199+
if (cnt > TB_RECURSIVE_CUTOFF) {
200+
err = tb_print_line_repeated(&writer, cnt);
201+
}
202+
last_file = tb->tb_frame->f_code->co_filename;
203+
last_line = tb->tb_lineno;
204+
last_name = tb->tb_frame->f_code->co_name;
205+
cnt = 0;
206+
}
207+
cnt++;
208+
if (err == 0 && cnt <= TB_RECURSIVE_CUTOFF) {
209+
fileName = tb->tb_frame->f_code->co_filename;
210+
lineno = tb->tb_lineno;
211+
212+
line = PyUnicode_FromFormat("File \"%U\", line %d, in %U\n", fileName, lineno, tb->tb_frame->f_code->co_name);
213+
if (line == NULL) {
214+
goto error;
215+
}
216+
217+
int res = _PyUnicodeWriter_WriteStr(&writer, line);
218+
Py_DECREF(line);
219+
if (res < 0) {
220+
goto error;
221+
}
222+
}
223+
tb = tb->tb_next;
224+
}
225+
if (err == 0 && cnt > TB_RECURSIVE_CUTOFF) {
226+
err = tb_print_line_repeated(&writer, cnt);
227+
}
228+
229+
if (err) {
230+
goto error;
231+
}
232+
233+
#endif
234+
235+
{
236+
std::stringstream msgStream;
237+
msgStream << "Python " << pyErrTypeName << ": " << PyUnicode_AsUTF8(pyErrMsg) << "\n" << PyUnicode_AsUTF8(_PyUnicodeWriter_Finish(&writer));
238+
std::string msg = msgStream.str();
239+
240+
JS::RootedValue rval(cx);
241+
JS::RootedString filename(cx, JS_NewStringCopyZ(cx, PyUnicode_AsUTF8(fileName)));
242+
JS::RootedString message(cx, JS_NewStringCopyZ(cx, msg.c_str()));
243+
// TODO stack argument cannot be passed in as a string anymore (deprecated), and could not find a proper example using the new argument type
244+
if (!JS::CreateError(cx, JSExnType::JSEXN_ERR, nullptr, filename, lineno, 0, nullptr, message, JS::NothingHandleValue, &rval)) {
245+
return NULL;
246+
}
247+
248+
Py_DECREF(pyErrType);
249+
Py_DECREF(pyErrMsg);
250+
251+
return rval.toObjectOrNull();
252+
}
253+
254+
error:
255+
_PyUnicodeWriter_Dealloc(&writer);
256+
Py_XDECREF(code);
257+
}
36258

37259
std::stringstream msgStream;
38260
msgStream << "Python " << pyErrTypeName << ": " << PyUnicode_AsUTF8(pyErrMsg);
@@ -42,10 +264,12 @@ JSObject *ExceptionType::toJsError(JSContext *cx) {
42264
JS::RootedObject stack(cx);
43265
JS::RootedString filename(cx, JS_NewStringCopyZ(cx, "[python code]"));
44266
JS::RootedString message(cx, JS_NewStringCopyZ(cx, msg.c_str()));
45-
JS::CreateError(cx, JSExnType::JSEXN_ERR, stack, filename, 0, 0, nullptr, message, JS::NothingHandleValue, &rval);
267+
if (!JS::CreateError(cx, JSExnType::JSEXN_ERR, nullptr, filename, 0, 0, nullptr, message, JS::NothingHandleValue, &rval)) {
268+
return NULL;
269+
}
46270

47271
Py_DECREF(pyErrType);
48272
Py_DECREF(pyErrMsg);
49273

50274
return rval.toObjectOrNull();
51-
}
275+
}

src/jsTypeFactory.cc

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
/**
22
* @file jsTypeFactory.cc
3-
* @author Caleb Aikens ([email protected])
3+
* @author Caleb Aikens ([email protected]) and Philippe Laporte ([email protected])
44
* @brief
5-
* @version 0.1
65
* @date 2023-02-15
76
*
8-
* @copyright Copyright (c) 2023
7+
* @copyright 2023-2024 Distributive Corp.
98
*
109
*/
1110

@@ -149,8 +148,13 @@ JS::Value jsTypeFactory(JSContext *cx, PyObject *object) {
149148
Py_INCREF(object); // otherwise the python function object would be double-freed on GC in Python 3.11+
150149
}
151150
else if (PyExceptionInstance_Check(object)) {
152-
JSObject *error = ExceptionType(object).toJsError(cx);
153-
returnType.setObject(*error);
151+
JSObject *error = ExceptionType::toJsError(cx, object, nullptr);
152+
if (error) {
153+
returnType.setObject(*error);
154+
}
155+
else {
156+
returnType.setUndefined();
157+
}
154158
}
155159
else if (PyDateTime_Check(object)) {
156160
JSObject *dateObj = DateType(object).toJsDate(cx);
@@ -231,9 +235,16 @@ void setPyException(JSContext *cx) {
231235
PyObject *type, *value, *traceback;
232236
PyErr_Fetch(&type, &value, &traceback); // also clears the error indicator
233237

234-
JSObject *jsException = ExceptionType(value).toJsError(cx);
235-
JS::RootedValue jsExceptionValue(cx, JS::ObjectValue(*jsException));
236-
JS_SetPendingException(cx, jsExceptionValue);
238+
JSObject *jsException = ExceptionType::toJsError(cx, value, traceback);
239+
240+
Py_XDECREF(type);
241+
Py_XDECREF(value);
242+
Py_XDECREF(traceback);
243+
244+
if (jsException) {
245+
JS::RootedValue jsExceptionValue(cx, JS::ObjectValue(*jsException));
246+
JS_SetPendingException(cx, jsExceptionValue);
247+
}
237248
}
238249

239250
bool callPyFunc(JSContext *cx, unsigned int argc, JS::Value *vp) {
@@ -282,9 +293,11 @@ bool callPyFunc(JSContext *cx, unsigned int argc, JS::Value *vp) {
282293
// @TODO (Caleb Aikens) need to check for python exceptions here
283294
callargs.rval().set(jsTypeFactory(cx, pyRval));
284295
if (PyErr_Occurred()) {
296+
Py_DECREF(pyRval);
285297
setPyException(cx);
286298
return false;
287299
}
288300

301+
Py_DECREF(pyRval);
289302
return true;
290303
}

tests/python/test_pythonmonkey_eval.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,10 @@ def c():
129129
return "Caught in JS " + e;
130130
}
131131
}''')
132-
assert b(c) == "Caught in JS Error: Python Exception: this is an exception"
132+
assert str(b(c)).__contains__("Caught in JS Error: Python Exception: this is an exception")
133+
assert str(b(c)).__contains__("test_pythonmonkey_eval.py")
134+
assert str(b(c)).__contains__("line 124")
135+
assert str(b(c)).__contains__("in c")
133136

134137
def test_eval_exceptions_nested_js_py_js():
135138
c = pm.eval("() => { throw TypeError('this is an exception'); }")

0 commit comments

Comments
 (0)