Skip to content

Commit d747ccd

Browse files
committed
Add native extensions to Python wrapper
1 parent 4876d97 commit d747ccd

File tree

4 files changed

+219
-17
lines changed

4 files changed

+219
-17
lines changed

python/_jsonnet.c

Lines changed: 186 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,70 @@ static char *jsonnet_str(struct JsonnetVm *vm, const char *str)
2828
return out;
2929
}
3030

31+
static const char *exc_to_str(void)
32+
{
33+
PyObject *ptype, *pvalue, *ptraceback;
34+
PyErr_Fetch(&ptype, &pvalue, &ptraceback);
35+
PyObject *exc_str = PyObject_Str(pvalue);
36+
return PyString_AsString(exc_str);
37+
}
38+
39+
struct NativeCtx {
40+
struct JsonnetVm *vm;
41+
PyObject *callback;
42+
size_t argc;
43+
};
44+
45+
/* This function is bound for every native callback, but with a different
46+
* context.
47+
*/
48+
static struct JsonnetJsonValue *cpython_native_callback(
49+
void *ctx_, const struct JsonnetJsonValue * const *argv, int *succ)
50+
{
51+
const struct NativeCtx *ctx = ctx_;
52+
int i;
53+
54+
PyObject *arglist; // Will hold a tuple of strings.
55+
PyObject *result; // Will hold a string.
56+
57+
// Populate python function args.
58+
arglist = PyTuple_New(ctx->argc);
59+
for (i = 0; i < ctx->argc; ++i) {
60+
const char *param = jsonnet_json_extract_string(ctx->vm, argv[i]);
61+
if (param == NULL) {
62+
Py_DECREF(arglist);
63+
*succ = 0;
64+
return jsonnet_json_make_string(ctx->vm, "Non-string param.");
65+
}
66+
PyTuple_SetItem(arglist, i, PyString_FromString(param));
67+
}
68+
69+
// Call python function.
70+
result = PyEval_CallObject(ctx->callback, arglist);
71+
Py_DECREF(arglist);
72+
73+
if (result == NULL) {
74+
// Get string from exception.
75+
struct JsonnetJsonValue *r = jsonnet_json_make_string(ctx->vm, exc_to_str());
76+
*succ = 0;
77+
PyErr_Clear();
78+
return r;
79+
}
80+
81+
if (!PyString_Check(result)) {
82+
struct JsonnetJsonValue *r =
83+
jsonnet_json_make_string(ctx->vm, "Python function did not return string");
84+
*succ = 0;
85+
return r;
86+
}
87+
88+
struct JsonnetJsonValue *r =
89+
jsonnet_json_make_string(ctx->vm, PyString_AsString(result));
90+
*succ = 1;
91+
return r;
92+
}
93+
94+
3195
struct ImportCtx {
3296
struct JsonnetVm *vm;
3397
PyObject *callback;
@@ -46,13 +110,7 @@ static char *cpython_import_callback(void *ctx_, const char *base, const char *r
46110

47111
if (result == NULL) {
48112
// Get string from exception
49-
PyObject *ptype;
50-
PyObject *pvalue;
51-
PyObject *ptraceback;
52-
PyErr_Fetch(&ptype, &pvalue, &ptraceback);
53-
PyObject *exc_str = PyObject_Str(pvalue);
54-
const char *exc_cstr = PyString_AsString(exc_str);
55-
char *out = jsonnet_str(ctx->vm, exc_cstr);
113+
char *out = jsonnet_str(ctx->vm, exc_to_str());
56114
*success = 0;
57115
PyErr_Clear();
58116
return out;
@@ -136,6 +194,7 @@ int handle_import_callback(struct ImportCtx *ctx, PyObject *import_callback)
136194
if (import_callback == NULL) return 1;
137195

138196
if (!PyCallable_Check(import_callback)) {
197+
jsonnet_destroy(ctx->vm);
139198
PyErr_SetString(PyExc_TypeError, "import_callback must be callable");
140199
return 0;
141200
}
@@ -146,6 +205,104 @@ int handle_import_callback(struct ImportCtx *ctx, PyObject *import_callback)
146205
}
147206

148207

208+
/** Register native callbacks with Jsonnet VM.
209+
*
210+
* Example native_callbacks = { 'name': (('p1', 'p2', 'p3'), func) }
211+
*
212+
* May set *ctxs, in which case it should be free()'d by caller.
213+
*
214+
* \returns 1 on success, 0 with exception set upon failure.
215+
*/
216+
static int handle_native_callbacks(struct JsonnetVm *vm, PyObject *native_callbacks,
217+
struct NativeCtx **ctxs)
218+
{
219+
size_t num_natives = 0;
220+
PyObject *key, *val;
221+
Py_ssize_t pos = 0;
222+
223+
if (native_callbacks == NULL) return 1;
224+
225+
/* Verify the input before we allocate memory, throw all errors at this point.
226+
* Also, count the callbacks to see how much memory we need.
227+
*/
228+
while (PyDict_Next(native_callbacks, &pos, &key, &val)) {
229+
Py_ssize_t i;
230+
Py_ssize_t num_params;
231+
PyObject *params;
232+
const char *key_ = PyString_AsString(key);
233+
if (key_ == NULL) {
234+
PyErr_SetString(PyExc_TypeError, "native callback dict keys must be string");
235+
goto bad;
236+
}
237+
if (!PyTuple_Check(val)) {
238+
PyErr_SetString(PyExc_TypeError, "native callback dict values must be tuples");
239+
goto bad;
240+
} else if (PyTuple_Size(val) != 2) {
241+
PyErr_SetString(PyExc_TypeError, "native callback tuples must have size 2");
242+
goto bad;
243+
}
244+
params = PyTuple_GetItem(val, 0);
245+
if (!PyTuple_Check(params)) {
246+
PyErr_SetString(PyExc_TypeError, "native callback params must be a tuple");
247+
goto bad;
248+
}
249+
/* Check the params are all strings */
250+
num_params = PyTuple_Size(params);
251+
for (i = 0; i < num_params ; ++i) {
252+
PyObject *param = PyTuple_GetItem(params, 0);
253+
if (!PyString_Check(param)) {
254+
PyErr_SetString(PyExc_TypeError, "native callback param must be string");
255+
goto bad;
256+
}
257+
}
258+
if (!PyCallable_Check(PyTuple_GetItem(val, 1))) {
259+
PyErr_SetString(PyExc_TypeError, "native callback must be callable");
260+
goto bad;
261+
}
262+
263+
num_natives++;
264+
continue;
265+
266+
bad:
267+
jsonnet_destroy(vm);
268+
return 0;
269+
}
270+
271+
if (num_natives == 0) {
272+
return 1;
273+
}
274+
275+
*ctxs = malloc(sizeof(struct NativeCtx) * num_natives);
276+
277+
/* Re-use num_natives but just as a counter this time. */
278+
num_natives = 0;
279+
pos = 0;
280+
while (PyDict_Next(native_callbacks, &pos, &key, &val)) {
281+
Py_ssize_t i;
282+
Py_ssize_t num_params;
283+
PyObject *params;
284+
const char *key_ = PyString_AsString(key);
285+
params = PyTuple_GetItem(val, 0);
286+
num_params = PyTuple_Size(params);
287+
/* Include space for terminating NULL. */
288+
const char **params_c = malloc(sizeof(const char*) * (num_params + 1));
289+
for (i = 0; i < num_params ; ++i) {
290+
params_c[i] = PyString_AsString(PyTuple_GetItem(params, i));
291+
}
292+
params_c[num_params] = NULL;
293+
(*ctxs)[num_natives].vm = vm;
294+
(*ctxs)[num_natives].callback = PyTuple_GetItem(val, 1);
295+
(*ctxs)[num_natives].argc = num_params;
296+
jsonnet_native_callback(vm, key_, cpython_native_callback, &(*ctxs)[num_natives],
297+
params_c);
298+
free(params_c);
299+
num_natives++;
300+
}
301+
302+
return 1;
303+
}
304+
305+
149306
static PyObject* evaluate_file(PyObject* self, PyObject* args, PyObject *keywds)
150307
{
151308
const char *filename;
@@ -156,21 +313,24 @@ static PyObject* evaluate_file(PyObject* self, PyObject* args, PyObject *keywds)
156313
PyObject *ext_vars = NULL, *ext_codes = NULL;
157314
PyObject *tla_vars = NULL, *tla_codes = NULL;
158315
PyObject *import_callback = NULL;
316+
PyObject *native_callbacks = NULL;
159317
struct JsonnetVm *vm;
160318
static char *kwlist[] = {
161319
"filename",
162320
"max_stack", "gc_min_objects", "gc_growth_trigger", "ext_vars",
163321
"ext_codes", "tla_vars", "tla_codes", "max_trace", "import_callback",
322+
"native_callbacks",
164323
NULL
165324
};
166325

167326
(void) self;
168327

169328
if (!PyArg_ParseTupleAndKeywords(
170-
args, keywds, "s|IIdOOOOIO", kwlist,
329+
args, keywds, "s|IIdOOOOIOO", kwlist,
171330
&filename,
172331
&max_stack, &gc_min_objects, &gc_growth_trigger, &ext_vars,
173-
&ext_codes, &tla_vars, &tla_codes, &max_trace, &import_callback)) {
332+
&ext_codes, &tla_vars, &tla_codes, &max_trace, &import_callback,
333+
&native_callbacks)) {
174334
return NULL;
175335
}
176336

@@ -187,8 +347,13 @@ static PyObject* evaluate_file(PyObject* self, PyObject* args, PyObject *keywds)
187347
if (!handle_import_callback(&ctx, import_callback)) {
188348
return NULL;
189349
}
190-
350+
struct NativeCtx *ctxs = NULL;
351+
if (!handle_native_callbacks(vm, native_callbacks, &ctxs)) {
352+
free(ctxs);
353+
return NULL;
354+
}
191355
out = jsonnet_evaluate_file(vm, filename, &error);
356+
free(ctxs);
192357
return handle_result(vm, out, error);
193358
}
194359

@@ -202,21 +367,24 @@ static PyObject* evaluate_snippet(PyObject* self, PyObject* args, PyObject *keyw
202367
PyObject *ext_vars = NULL, *ext_codes = NULL;
203368
PyObject *tla_vars = NULL, *tla_codes = NULL;
204369
PyObject *import_callback = NULL;
370+
PyObject *native_callbacks = NULL;
205371
struct JsonnetVm *vm;
206372
static char *kwlist[] = {
207373
"filename", "src",
208374
"max_stack", "gc_min_objects", "gc_growth_trigger", "ext_vars",
209375
"ext_codes", "tla_vars", "tla_codes", "max_trace", "import_callback",
376+
"native_callbacks",
210377
NULL
211378
};
212379

213380
(void) self;
214381

215382
if (!PyArg_ParseTupleAndKeywords(
216-
args, keywds, "ss|IIdOOOOIO", kwlist,
383+
args, keywds, "ss|IIdOOOOIOO", kwlist,
217384
&filename, &src,
218385
&max_stack, &gc_min_objects, &gc_growth_trigger, &ext_vars,
219-
&ext_codes, &tla_vars, &tla_codes, &max_trace, &import_callback)) {
386+
&ext_codes, &tla_vars, &tla_codes, &max_trace, &import_callback,
387+
&native_callbacks)) {
220388
return NULL;
221389
}
222390

@@ -233,8 +401,13 @@ static PyObject* evaluate_snippet(PyObject* self, PyObject* args, PyObject *keyw
233401
if (!handle_import_callback(&ctx, import_callback)) {
234402
return NULL;
235403
}
236-
404+
struct NativeCtx *ctxs = NULL;
405+
if (!handle_native_callbacks(vm, native_callbacks, &ctxs)) {
406+
free(ctxs);
407+
return NULL;
408+
}
237409
out = jsonnet_evaluate_snippet(vm, filename, src, &error);
410+
free(ctxs);
238411
return handle_result(vm, out, error);
239412
}
240413

python/jsonnet_test_file.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,17 @@ def import_callback(dir, rel):
4343
return full_path, content
4444
raise RuntimeError('File not found')
4545

46-
sys.stdout.write(_jsonnet.evaluate_file(sys.argv[1], import_callback=import_callback))
46+
# Test native extensions
47+
def concat(a, b):
48+
return a + b
49+
50+
native_callbacks = {
51+
'concat': (('a', 'b'), concat),
52+
}
53+
54+
json_str = _jsonnet.evaluate_file(
55+
sys.argv[1],
56+
import_callback=import_callback,
57+
native_callbacks=native_callbacks,
58+
)
59+
sys.stdout.write(json_str)

python/jsonnet_test_snippet.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
if len(sys.argv) != 2:
2121
raise Exception("Usage: <snippet>")
2222

23-
# Returns content if worked, None if file not found, or throws an exception
23+
# Returns content if worked, None if file not found, or throws an exception
2424
def try_path(dir, rel):
2525
if not rel:
2626
raise RuntimeError('Got invalid filename (empty string).')
@@ -43,4 +43,18 @@ def import_callback(dir, rel):
4343
return full_path, content
4444
raise RuntimeError('File not found')
4545

46-
sys.stdout.write(_jsonnet.evaluate_snippet("snippet", sys.argv[1], import_callback=import_callback))
46+
# Test native extensions
47+
def concat(a, b):
48+
return a + b
49+
50+
native_callbacks = {
51+
'concat': (('a', 'b'), concat),
52+
}
53+
54+
json_str = _jsonnet.evaluate_snippet(
55+
"snippet",
56+
sys.argv[1],
57+
import_callback=import_callback,
58+
native_callbacks=native_callbacks,
59+
)
60+
sys.stdout.write(json_str)

python/test.jsonnet

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
1-
std.assertEqual(({ x: 1, y: self.x } { x: 2 }).y, 2)
1+
std.assertEqual(({ x: 1, y: self.x } { x: 2 }).y, 2) &&
2+
std.assertEqual(std.native("concat")("foo", "bar"), "foobar") &&
3+
true
24

0 commit comments

Comments
 (0)