22
33#include "third-party/quickjs.h"
44
5+ // The data of the type _quickjs.Context.
56typedef struct {
67 PyObject_HEAD JSRuntime * runtime ;
78 JSContext * context ;
89} ContextData ;
910
11+ // The data of the type _quickjs.Object.
1012typedef struct {
1113 PyObject_HEAD ;
1214 ContextData * context ;
1315 JSValue object ;
1416} ObjectData ;
1517
18+ // The exception raised by this module.
1619static 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).
1723static PyObject * quickjs_to_python (ContextData * context_obj , JSValue value );
1824
19- //
20- // Object type
21- //
25+ // Creates an instance of the Object class.
2226static 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.
3136static 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__
3947static 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.
4152static 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.
5470static 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.
5976static 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__
6987static 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).
106138static 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.
154193struct module_state {};
155194
156- //
157- // Context type
158- //
195+ // Creates an instance of the _quickjs.Context class.
159196static 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.
169209static 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).
175219static 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.
187238static 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.
198250static 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.
204257static 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.
213267static PyMethodDef myextension_methods [] = {{"test" , (PyCFunction )test , METH_NOARGS , NULL },
214268 {NULL , NULL }};
215269
270+ // Define the _quickjs module.
216271static 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.
226282PyMODINIT_FUNC PyInit__quickjs (void ) {
227283 if (PyType_Ready (& Context ) < 0 ) {
228284 return NULL ;
0 commit comments