calling python function in C

C programming, build, interpreter/VM.
Target audience: MicroPython Developers.
Post Reply
v923z
Posts: 168
Joined: Mon Dec 28, 2015 6:19 pm

calling python function in C

Post by v923z » Tue Apr 28, 2020 5:35 am

Hi all,

I would like to ask, whether it is possible to call a function that was defined in python at the C level. Here is the context: in ulab, the functions of the micropython math module are vectorised, i.e., one can do

Code: Select all

import ulab
from ulab import vector

a = ulab.array([1, 2, 3, 4])
b = vector.sin(a)
and the function in b = vector.sin(a) is called exactly once in python, and the iteration is done in C.

How could one extend this idea to user-defined functions, like so:

Code: Select all

import ulab
from ulab import vector

def f(x):
	return sin(x) + x*x
	
a = ulab.array([1, 2, 3, 4])
b = f(a)
This would be especially interesting for non-linear curve fitting and root finding.

I know that I could do something like this:

Code: Select all

...
mp_call_fun_t fun = MICROPY_MAKE_POINTER_CALLABLE((void *)user_function->bytecode);

mp_float_t *array = (mp_float_t *)input_array->array->items;

for(size_t i=0; i < input_array->array->len; i++) {
	mp_obj_t arg = mp_obj_new_float(*array++);
	mp_obj_t results = fun(arg);
	mp_float_t f_result = mp_obj_get_float(result);
	...
}
...
but that is not ideal, because fun wants to have arguments of mp_obj_t type, and returns values of mp_obj_t type, i.e., one has to convert to and from mp_obj_t, and this would not be much better than the simple list comprehension

Code: Select all

[f(x) for x in input_array]
Is there a way to save the round trip to mp_obj_t?

Thanks,

Zoltán

User avatar
jimmo
Posts: 2754
Joined: Tue Aug 08, 2017 1:57 am
Location: Sydney, Australia
Contact:

Re: calling python function in C

Post by jimmo » Tue Apr 28, 2020 6:21 am

v923z wrote:
Tue Apr 28, 2020 5:35 am
I would like to ask, whether it is possible to call a function that was defined in python at the C level.
The simple answer is that yes this is done all over the place via mp_call_function_n_kw (and the wrappers e.g. mp_call_function_0).

Given an mp_obj_t that points to a function (i.e. a "callback" argument or something), you can just invoke it using mp_call_function_*.

But I don't think that's actually what you're asking?
v923z wrote:
Tue Apr 28, 2020 5:35 am
How could one take this to user-defined functions, like so:

Code: Select all

import ulab
from ulab import vector

def f(x):
	return sin(x) + x*x
	
a = ulab.array([1, 2, 3, 4])
b = f(a)
In this case, if sin() was ulab.sin, and and __mul__ were provided by the array type, wouldn't this already work?

I'm not quite sure how this relates you your question as "f" is never passed to any ulab code.

Did you mean something more like:

Code: Select all

b = a.map(f)
and then the map function would invoke f on each element?

I guess the question is -- is the intention that f is called once with "a" and calls vector functions (I thought this would already work). Or f is called once per item in "a" (somehow MicroPython figures out that calling a single-arg function on an array means we need to vectorise "f").

Do you have a numpy example that shows the same thing?
v923z wrote:
Tue Apr 28, 2020 5:35 am
I know that I could do something like this:
...
mp_call_fun_t fun = MICROPY_MAKE_POINTER_CALLABLE((void *)user_function->bytecode);
MICROPY_MAKE_POINTER_CALLABLE makes a pointer to machine code into a callable function. This will not work as bytecode cannot be natively executed.
v923z wrote:
Tue Apr 28, 2020 5:35 am
Is there a way to save the round trip to mp_obj_t?
Not if you're executing Python bytecode with these objects. But for many types, the conversion to mp_obj_t is a shift and a single bitwise op.

v923z
Posts: 168
Joined: Mon Dec 28, 2015 6:19 pm

Re: calling python function in C

Post by v923z » Tue Apr 28, 2020 7:42 am

Jim,

Many thanks for your illuminating comments!
jimmo wrote:
Tue Apr 28, 2020 6:21 am
v923z wrote:
Tue Apr 28, 2020 5:35 am
I would like to ask, whether it is possible to call a function that was defined in python at the C level.
The simple answer is that yes this is done all over the place via mp_call_function_n_kw (and the wrappers e.g. mp_call_function_0).

Given an mp_obj_t that points to a function (i.e. a "callback" argument or something), you can just invoke it using mp_call_function_*.

But I don't think that's actually what you're asking?
Not exactly. I wanted to save the mp_obt_t <-> mp_float_t conversion steps, so that I can make the iteration efficient.
v923z wrote:
Tue Apr 28, 2020 5:35 am
How could one take this to user-defined functions, like so:
jimmo wrote:
Tue Apr 28, 2020 6:21 am

Code: Select all

import ulab
from ulab import vector

def f(x):
	return sin(x) + x*x
	
a = ulab.array([1, 2, 3, 4])
b = f(a)
In this case, if sin() was ulab.sin, and and __mul__ were provided by the array type, wouldn't this already work?
In this particular case, one could do as you suggest, that's true, but you still can't pass a parametrised function to fitting, or you might still want to pass a function object for root finding. That would mean that you have to either "disassemble" the function, or you have to take the compiled version of f, and then call it for each value that comes up during the Newton iteration.
jimmo wrote:
Tue Apr 28, 2020 6:21 am
Did you mean something more like:

Code: Select all

b = a.map(f)
and then the map function would invoke f on each element?
Yes, mapping is probably a better term.
jimmo wrote:
Tue Apr 28, 2020 6:21 am
I guess the question is -- is the intention that f is called once with "a" and calls vector functions (I thought this would already work).
The crux of the matter is that in

Code: Select all

import ulab
from ulab import vector

def f(x):
	return sin(x) + x*x
	
a = ulab.array([1, 2, 3, 4])
b = f(a)
the constituents of f are called at the python level, so you don't have access to the values of f in C. In other words, f puts together the results of two functions (sin, and *) that both return to python. In a sense, what I need is a way of preventing f from returning to the console, and supplying its results to another C function. Although, that might not be enough, because f produces an ndarray (as do sin, and *), so that might be inefficient as far as RAM is concerned.
jimmo wrote:
Tue Apr 28, 2020 6:21 am
Or f is called once per item in "a" (somehow MicroPython figures out that calling a single-arg function on an array means we need to vectorise "f").
Barring my last comment, this is what I am aiming for. These two are perhaps two separate, but related issues. It would be great, if both could be solved.
jimmo wrote:
Tue Apr 28, 2020 6:21 am
Do you have a numpy example that shows the same thing?
In very general terms https://docs.scipy.org/doc/numpy/refere ... orize.html is what I am after. But e.g., the newton iteration code is also similar: https://docs.scipy.org/doc/scipy/refere ... ewton.html, as are any of the fitting routines: https://docs.scipy.org/doc/scipy/refere ... e_fit.html

"Give me a function defined in python, and I will run efficient computations with it in C."
jimmo wrote:
Tue Apr 28, 2020 6:21 am
v923z wrote:
Tue Apr 28, 2020 5:35 am
I know that I could do something like this:
...
mp_call_fun_t fun = MICROPY_MAKE_POINTER_CALLABLE((void *)user_function->bytecode);
MICROPY_MAKE_POINTER_CALLABLE makes a pointer to machine code into a callable function. This will not work as bytecode cannot be natively executed.
Thanks for pointing that out! I would have been barking up the wrong tree.
v923z wrote:
Tue Apr 28, 2020 5:35 am
Is there a way to save the round trip to mp_obj_t?
jimmo wrote:
Tue Apr 28, 2020 6:21 am
But for many types, the conversion to mp_obj_t is a shift and a single bitwise op.

That's actually good, because then one can save the two function calls. That could already be OK. I have to be careful with the various representation, though, haven't I?

User avatar
jimmo
Posts: 2754
Joined: Tue Aug 08, 2017 1:57 am
Location: Sydney, Australia
Contact:

Re: calling python function in C

Post by jimmo » Wed Apr 29, 2020 12:39 am

v923z wrote:
Tue Apr 28, 2020 7:42 am
Not exactly. I wanted to save the mp_obt_t <-> mp_float_t conversion steps, so that I can make the iteration efficient.
This is just a pointer dereference with an offset. I'm not sure this is likely to be your highest efficiency concern.
v923z wrote:
Tue Apr 28, 2020 7:42 am
That's actually good, because then one can save the two function calls. That could already be OK. I have to be careful with the various representation, though, haven't I?
These are already macros, so they will be inlined. Definitely do not implement these conversions yourself. (Like you say, there are four different representations and they are internal details).
v923z wrote:
Tue Apr 28, 2020 7:42 am
"Give me a function defined in python, and I will run efficient computations with it in C."
I think I mostly understand, but it seems unavoidable to me that Python code has to run. I almost feel like the only way to improve this would be to have a different variation of the native emitter that was "numpy vectorisation" aware. (A huge undertaking)

I will PM you.

v923z
Posts: 168
Joined: Mon Dec 28, 2015 6:19 pm

Re: calling python function in C

Post by v923z » Wed May 06, 2020 9:28 am

Thanks again for the clarification!

I would like to ask a related question, namely, is there a way of finding out the number of (position) arguments a function requires? Say, I have

Code: Select all

def f(x, a, b):
	return a*x + b
How do I get 3? If I get a handle to the function through mp_obj_type_t *type = mp_obj_get_type(fun);, type has no reference to the number of arguments. Whenever functions are called, that seems to happen via mp_call_function_n_kw (https://github.com/micropython/micropyt ... ime.c#L635), but it is not clear to me at the moment, what feeds the mp_call_function_n_kw function with arguments.

User avatar
jimmo
Posts: 2754
Joined: Tue Aug 08, 2017 1:57 am
Location: Sydney, Australia
Contact:

Re: calling python function in C

Post by jimmo » Thu May 07, 2020 3:38 am

v923z wrote:
Wed May 06, 2020 9:28 am
How do I get 3? If I get a handle to the function through mp_obj_type_t *type = mp_obj_get_type(fun);, type has no reference to the number of arguments. Whenever functions are called, that seems to happen via mp_call_function_n_kw (https://github.com/micropython/micropyt ... ime.c#L635), but it is not clear to me at the moment, what feeds the mp_call_function_n_kw function with arguments.
Not really possible in a non-fragile way. See earlier discussion here viewtopic.php?f=3&t=6728&p=38316

v923z
Posts: 168
Joined: Mon Dec 28, 2015 6:19 pm

Re: calling python function in C

Post by v923z » Thu May 07, 2020 6:28 am

jimmo wrote:
Thu May 07, 2020 3:38 am
Not really possible in a non-fragile way.
Bit of a predicament, but still, thanks for the pointer!

Post Reply