Skip to content

Commit 9e698e8

Browse files
committed
Document the C code better.
1 parent 9a64734 commit 9e698e8

File tree

1 file changed

+63
-7
lines changed

1 file changed

+63
-7
lines changed

module.c

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,27 @@
22

33
#include "third-party/quickjs.h"
44

5+
// The data of the type _quickjs.Context.
56
typedef struct {
67
PyObject_HEAD JSRuntime *runtime;
78
JSContext *context;
89
} ContextData;
910

11+
// The data of the type _quickjs.Object.
1012
typedef struct {
1113
PyObject_HEAD;
1214
ContextData *context;
1315
JSValue object;
1416
} ObjectData;
1517

18+
// The exception raised by this module.
1619
static PyObject *JSException = NULL;
20+
// Converts a JSValue to a Python object.
21+
//
22+
// Takes ownership of the JSValue and will deallocate it (refcount reduced by 1).
1723
static PyObject *quickjs_to_python(ContextData *context_obj, JSValue value);
1824

19-
//
20-
// Object type
21-
//
25+
// Creates an instance of the Object class.
2226
static PyObject *object_new(PyTypeObject *type, PyObject *args, PyObject *kwds) {
2327
ObjectData *self;
2428
self = (ObjectData *)type->tp_alloc(type, 0);
@@ -28,48 +32,66 @@ static PyObject *object_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
2832
return (PyObject *)self;
2933
}
3034

35+
// Deallocates an instance of the Object class.
3136
static void object_dealloc(ObjectData *self) {
3237
if (self->context) {
3338
JS_FreeValue(self->context->context, self->object);
39+
// We incremented the refcount of the context when we created this object, so we should
40+
// decrease it now so we don't leak memory.
3441
Py_DECREF(self->context);
3542
}
3643
Py_TYPE(self)->tp_free((PyObject *)self);
3744
}
3845

46+
// _quickjs.Object.__call__
3947
static PyObject *object_call(ObjectData *self, PyObject *args, PyObject *kwds);
4048

49+
// _quickjs.Object.json
50+
//
51+
// Returns the JSON representation of the object as a Python string.
4152
static PyObject *object_json(ObjectData *self) {
53+
// Use the JS JSON.stringify method to convert to JSON. First, we need to retrieve it via
54+
// API calls.
4255
JSContext *context = self->context->context;
4356
JSValue global = JS_GetGlobalObject(context);
4457
JSValue JSON = JS_GetPropertyStr(context, global, "JSON");
4558
JSValue stringify = JS_GetPropertyStr(context, JSON, "stringify");
59+
4660
JSValueConst args[1] = {self->object};
4761
JSValue json_string = JS_Call(context, stringify, JSON, 1, args);
62+
4863
JS_FreeValue(context, global);
4964
JS_FreeValue(context, JSON);
5065
JS_FreeValue(context, stringify);
5166
return quickjs_to_python(self->context, json_string);
5267
}
5368

69+
// All methods of the _quickjs.Object class.
5470
static PyMethodDef object_methods[] = {
5571
{"json", (PyCFunction)object_json, METH_NOARGS, "Converts to a JSON string."},
5672
{NULL} /* Sentinel */
5773
};
5874

75+
// Define the quickjs.Object type.
5976
static PyTypeObject Object = {PyVarObject_HEAD_INIT(NULL, 0).tp_name = "_quickjs.Object",
6077
.tp_doc = "Quickjs object",
6178
.tp_basicsize = sizeof(ObjectData),
6279
.tp_itemsize = 0,
6380
.tp_flags = Py_TPFLAGS_DEFAULT,
6481
.tp_new = object_new,
6582
.tp_dealloc = (destructor)object_dealloc,
66-
.tp_call = object_call,
83+
.tp_call = (ternaryfunc)object_call,
6784
.tp_methods = object_methods};
6885

86+
// _quickjs.Object.__call__
6987
static PyObject *object_call(ObjectData *self, PyObject *args, PyObject *kwds) {
7088
if (self->context == NULL) {
89+
// This object does not have a context and has not been created by this module.
7190
Py_RETURN_NONE;
7291
}
92+
93+
// We first loop through all arguments and check that they are supported without doing anything.
94+
// This makes the cleanup code simpler for the case where we have to raise an error.
7395
const int nargs = PyTuple_Size(args);
7496
for (int i = 0; i < nargs; ++i) {
7597
PyObject *item = PyTuple_GetItem(args, i);
@@ -81,6 +103,8 @@ static PyObject *object_call(ObjectData *self, PyObject *args, PyObject *kwds) {
81103
return NULL;
82104
}
83105
}
106+
107+
// Now we know that all arguments are supported and we can convert them.
84108
JSValueConst *jsargs = malloc(nargs * sizeof(JSValueConst));
85109
for (int i = 0; i < nargs; ++i) {
86110
PyObject *item = PyTuple_GetItem(args, i);
@@ -92,20 +116,29 @@ static PyObject *object_call(ObjectData *self, PyObject *args, PyObject *kwds) {
92116
jsargs[i] = JS_DupValue(self->context->context, ((ObjectData *)item)->object);
93117
}
94118
}
119+
120+
// Perform the actual function call. We release the GIL in order to speed things up for certain
121+
// use cases. If this module becomes more complicated and gains the capability to call Python
122+
// function from JS, this needs to be reversed or improved.
95123
JSValue value;
96124
Py_BEGIN_ALLOW_THREADS;
97125
value = JS_Call(self->context->context, self->object, JS_NULL, nargs, jsargs);
98126
Py_END_ALLOW_THREADS;
127+
99128
for (int i = 0; i < nargs; ++i) {
100129
JS_FreeValue(self->context->context, jsargs[i]);
101130
}
102131
free(jsargs);
103132
return quickjs_to_python(self->context, value);
104133
}
105134

135+
// Converts a JSValue to a Python object.
136+
//
137+
// Takes ownership of the JSValue and will deallocate it (refcount reduced by 1).
106138
static PyObject *quickjs_to_python(ContextData *context_obj, JSValue value) {
107139
JSContext *context = context_obj->context;
108140
int tag = JS_VALUE_GET_TAG(value);
141+
// A return value of NULL means an exception.
109142
PyObject *return_value = NULL;
110143

111144
if (tag == JS_TAG_INT) {
@@ -117,6 +150,7 @@ static PyObject *quickjs_to_python(ContextData *context_obj, JSValue value) {
117150
} else if (tag == JS_TAG_UNDEFINED) {
118151
return_value = Py_None;
119152
} else if (tag == JS_TAG_EXCEPTION) {
153+
// We have a Javascript exception. We convert it to a Python exception via a C string.
120154
JSValue exception = JS_GetException(context);
121155
JSValue error_string = JS_ToString(context, exception);
122156
const char *cstring = JS_ToCString(context, error_string);
@@ -131,8 +165,11 @@ static PyObject *quickjs_to_python(ContextData *context_obj, JSValue value) {
131165
return_value = Py_BuildValue("s", cstring);
132166
JS_FreeCString(context, cstring);
133167
} else if (tag == JS_TAG_OBJECT) {
168+
// This is a Javascript object or function. We wrap it in a _quickjs.Object.
134169
return_value = PyObject_CallObject((PyObject *)&Object, NULL);
135170
ObjectData *object = (ObjectData *)return_value;
171+
// This is important. Otherwise, the context may be deallocated before the object, which
172+
// will result in a segfault with high probability.
136173
Py_INCREF(context_obj);
137174
object->context = context_obj;
138175
object->object = JS_DupValue(context, value);
@@ -142,6 +179,7 @@ static PyObject *quickjs_to_python(ContextData *context_obj, JSValue value) {
142179

143180
JS_FreeValue(context, value);
144181
if (return_value == Py_None) {
182+
// Can not simply return PyNone for refcounting reasons.
145183
Py_RETURN_NONE;
146184
}
147185
return return_value;
@@ -151,39 +189,52 @@ static PyObject *test(PyObject *self, PyObject *args) {
151189
return Py_BuildValue("i", 42);
152190
}
153191

192+
// Global state of the module. Currently none.
154193
struct module_state {};
155194

156-
//
157-
// Context type
158-
//
195+
// Creates an instance of the _quickjs.Context class.
159196
static PyObject *context_new(PyTypeObject *type, PyObject *args, PyObject *kwds) {
160197
ContextData *self;
161198
self = (ContextData *)type->tp_alloc(type, 0);
162199
if (self != NULL) {
200+
// We never have different contexts for the same runtime. This way, different
201+
// _quickjs.Context can be used concurrently.
163202
self->runtime = JS_NewRuntime();
164203
self->context = JS_NewContext(self->runtime);
165204
}
166205
return (PyObject *)self;
167206
}
168207

208+
// Deallocates an instance of the _quickjs.Context class.
169209
static void context_dealloc(ContextData *self) {
170210
JS_FreeContext(self->context);
171211
JS_FreeRuntime(self->runtime);
172212
Py_TYPE(self)->tp_free((PyObject *)self);
173213
}
174214

215+
// _quickjs.Context.eval
216+
//
217+
// Evaluates a Python string as JS and returns the result as a Python object. Will return
218+
// _quickjs.Object for complex types (other than e.g. str, int).
175219
static PyObject *context_eval(ContextData *self, PyObject *args) {
176220
const char *code;
177221
if (!PyArg_ParseTuple(args, "s", &code)) {
178222
return NULL;
179223
}
224+
225+
// Perform the actual evaluation. We release the GIL in order to speed things up for certain
226+
// use cases. If this module becomes more complicated and gains the capability to call Python
227+
// function from JS, this needs to be reversed or improved.
180228
JSValue value;
181229
Py_BEGIN_ALLOW_THREADS;
182230
value = JS_Eval(self->context, code, strlen(code), "<input>", JS_EVAL_TYPE_GLOBAL);
183231
Py_END_ALLOW_THREADS;
184232
return quickjs_to_python(self, value);
185233
}
186234

235+
// _quickjs.Context.get
236+
//
237+
// Retrieves a global variable from the JS context.
187238
static PyObject *context_get(ContextData *self, PyObject *args) {
188239
const char *name;
189240
if (!PyArg_ParseTuple(args, "s", &name)) {
@@ -195,12 +246,14 @@ static PyObject *context_get(ContextData *self, PyObject *args) {
195246
return quickjs_to_python(self, value);
196247
}
197248

249+
// All methods of the _quickjs.Context class.
198250
static PyMethodDef context_methods[] = {
199251
{"eval", (PyCFunction)context_eval, METH_VARARGS, "Evaluates a Javascript string."},
200252
{"get", (PyCFunction)context_get, METH_VARARGS, "Gets a Javascript global variable."},
201253
{NULL} /* Sentinel */
202254
};
203255

256+
// Define the _quickjs.Context type.
204257
static PyTypeObject Context = {PyVarObject_HEAD_INIT(NULL, 0).tp_name = "_quickjs.Context",
205258
.tp_doc = "Quickjs context",
206259
.tp_basicsize = sizeof(ContextData),
@@ -210,9 +263,11 @@ static PyTypeObject Context = {PyVarObject_HEAD_INIT(NULL, 0).tp_name = "_quickj
210263
.tp_dealloc = (destructor)context_dealloc,
211264
.tp_methods = context_methods};
212265

266+
// All global methods in _quickjs.
213267
static PyMethodDef myextension_methods[] = {{"test", (PyCFunction)test, METH_NOARGS, NULL},
214268
{NULL, NULL}};
215269

270+
// Define the _quickjs module.
216271
static struct PyModuleDef moduledef = {PyModuleDef_HEAD_INIT,
217272
"quickjs",
218273
NULL,
@@ -223,6 +278,7 @@ static struct PyModuleDef moduledef = {PyModuleDef_HEAD_INIT,
223278
NULL,
224279
NULL};
225280

281+
// This function runs when the module is first imported.
226282
PyMODINIT_FUNC PyInit__quickjs(void) {
227283
if (PyType_Ready(&Context) < 0) {
228284
return NULL;

0 commit comments

Comments
 (0)