C Language Interface¶
Zerynth OS allows mixing Python and C code in the same project. The Python language is compiled to bytecode and executed by the zOS independently of the target hardware; C language is compiled to object code dependent on the target hardware instruction set.
This kind of “hybrid” programming is extremely useful in those scenarios where the programmer needs to write or has already written performant low level code for time critical tasks, but wants to retain Python flexibility and readability for non time critical sections.
Zerynth OS allows calling C functions from Python, but not (yet) Python functions from C. To call a C function from Python follow this procedure:
- Define an “empty” Python function decorated with
@c_native
(let’s call itpyfn
) - As arguments of
@c_native
pass the name of the C function to be called (let’s call itcfn
), the source files wherecfn
implementation resides and a list of C macros to be defined during compilation - Create the C source file containing
cfn
and declarecfn
using the C macroC_NATIVE()
At compile time, @c_native
parameters will be used to locate the C source code files and compile them with the appropriate macros defined. When pyfn
is called, the OS translates the Python call to a C call.
C function call example¶
A minimal example of C function calling from Python follows.
In Python:
# main.py
@c_native("my_c_function",["my_c_source.c"],[])
def my_py_function(a,b):
"""
a simple function that returns the sum of a and b, with a and b integers
"""
# just pass, the body of my_py_fun is ignored by the compiler
pass
In C:
// my_c_source.c
//include zerynth header
#include "zerynth.h"
C_NATIVE(my_c_function) {
C_NATIVE_UNWARN();
int32_t a,b;
a = PYC_ARG_INT(0); //convert Python argument a to C integer
b = PYC_ARG_INT(1); //convert Python argument b to C integer
MAKE_RESULT(PSMALLINT_NEW(a+b)); //set the Python result to the sum of a+b
return ERR_OK; //return ok, no exception raised
}
In main.py
the empty function my_py_function
is defined, decorated with @c_native
. The @c_native
decorator informs the compiler that the body of my_py_fun
will be a C function called my_c_function
implemented in the file my_c_source.c
in the same directory where main.py
is residing.
In my_c_source.c
the function my_c_function
is implemented. The macro C_NATIVE(my_c_function)
expands to:
err_t my_c_function(int32_t nargs, PObject *self, PObject **args, PObject **res)
where:
nargs
is the number of arguments passed tomy_py_function
self
is the self parameter in casemy_py_function
is a methodargs
is an array of PObject*, the generic structure used by the VM to represent Python objectsres
is a pointer to a PObject containing the result of the function call- the returned value is of type
err_t
The zOS passes Python arguments to the C function without touching them; it is responsibility of the C function to convert them as needed. To help the conversion a set of macros id provided.
The return value of function my_py_function
must be set into res
and must be of type PObject*
. The actual return value of my_c_function
is an error code indicating success (ERR_OK) or an exception that is subsequently raised by the zOS.
For more details on the low level zOS functions and macros available for hybrid programming, refer to zOS Guide.
Macros¶
Some utility macros are added by the header dile zerynth.h
.
C_NATIVE(fn)
: used to define the implementation of a C function callable from Python. It equals to:C_NATIVE_UNWARN
: silences C warnings about unusedC_NATIVE
arguments in the body of a C function callable from PythonZDEBUG(...)
: if the user definesZERYNTH_DEBUG
inconfig.yml
equal todebug
, this macro behaves like a printf, writing to the standard error of the zOS adding some information on the current function and line. Otherwise it does nothing.ZERROR(...)
: if the user definesZERYNTH_DEBUG
inconfig.yml
equal toerror
, this macro behaves like a printf, writing to the standard error of the zOS adding some information on the current function and line. Otherwise it does nothing.ZWARN(...)
: if the user definesZERYNTH_DEBUG
inconfig.yml
equal towarning
, this macro behaves like a printf, writing to the standard error of the zOS adding some information on the current function and line. Otherwise it does nothing.ZINFO(...)
: if the user definesZERYNTH_DEBUG
inconfig.yml
equal toinfo
, this macro behaves like a printf, writing to the standard error of the zOS adding some information on the current function and line. Otherwise it does nothing.PYC_ARG_INT(n)
: converts thenth
python argument to aint32_t
without checking the type.PYC_ARG_FLOAT(n)
: converts thenth
python argument to adouble
without checking the type.PYC_ARG_BUF(n)
: converts thenth
python argument to auint8_t*
without checking the type. Can be used to get the buffer of abytes
,bytearray
,short
,shortarray
andstring
.PYC_ARG_BUFLEN(n)
: return the length of thenth
python argument to aint32_t
without checking the type. The argument must be a sequence.PYC_ARG_BOOL(n)
: converts thenth
python argument to aint32_t
from a boolean without checking the type.PYC_ARG_IS_NONE(n)
: converts thenth
python argument to a non-zeroint32_t
if it isNone
.PYC_CHECK_NUM_ARGS(n)
: raiseTypeError
if the arguments passed from Python are notn
.PYC_CHECK_ARG_INTEGER(n)
: raiseTypeError
if thenth
arguments passed from Python is not of integer type.PYC_CHECK_ARG_FLOAT(n)
: raiseTypeError
if thenth
arguments passed from Python is not of float type.PYC_CHECK_ARG_BOOL(n)
: raiseTypeError
if thenth
arguments passed from Python is not of bool type.PYC_CHECK_ARG_BUFFER(n)
: raiseTypeError
if thenth
arguments passed from Python is not usable withPYC_ARG_BUF
.MAKE_RESULT(obj)
: set the Python result of the function toobj
.obj
must be ofPObject*
type.
Zerynth “C” Limitations¶
C functions callable from Python have some limitations:
- C standard library function are not available. Only the following subset can be used:
memcpy
,memset
,memmove
,memcmp
,memchr
malloc
,free
(implemented as macros calling the garbage collector allocator)strlen
Refer to zOS Guide for the available API.
Advanced Example¶
More advances uses cases for Python/C interaction are possible when arguments are of more complex types.
A checksum function is presented below. It returns the checksum of a string/bytes in a tuple with different representations.
# main.py
@c_native("to_checksum",["my_c_source.c"],[])
def checksum(b):
"""
a simple function that convert the sequence b to hexadecimal form
"""
pass
# pass a bytes
print(checksum(b'Zerynth'))
# pass a string
print(checksum("Zerynth"))
# pass a tuple
#include "zerynth.h"
const uint32_t MOD_ADLER = 65521;
uint32_t adler32(uint8_t *data, int32_t len)
/*
where data is the location of the data in physical memory and
len is the length of the data in bytes
*/
{
uint32_t a = 1, b = 0;
size_t index;
// Process each byte of the data in order
for (index = 0; index < len; ++index)
{
a = (a + data[index]) % MOD_ADLER;
b = (b + a) % MOD_ADLER;
}
return (b << 16) | a;
}
C_NATIVE(to_checksum) {
C_NATIVE_UNWARN();
//declare a pointer to the buffer of bytes passed by Python
uint8_t* buffer;
//also declare a variable to hold its length
int32_t buflen;
// let's first check that the type is correct, we expect a string, bytes or bytearray
PYC_CHECK_ARG_BUFFER(0); //first argument is index 0
// now get the pointer to the buffer
buffer = PYC_ARG_BUF(0);
//retrieve the length
buflen = PYC_ARG_BUFLEN(0);
//we have it all, let's calculate te checksum
uint32_t chksum = adler32(buffer, buflen);
//now for te sake of example we will return a tuple with different representations
// convert chksum to a Python integer
PObject *chksum_int = (PObject*)pinteger_new(chksum);
// convert chksum to a Python hexadecimal string
// declare a temporary buffer to hold the hexadecimal representation of chksum
uint8_t hex_temp_buffer[16];
//call the snprintf function of the zOS and get the length of the string
int hex_len = platform_snprintf(hex_temp_buffer, 16, "%x", chksum);
//now hex_temp_buffer contains the checksum in hexadecimal format
//let's convert it into a Python string
PObject *chksum_str = (PObject*)pstring_new(hex_len,hex_temp_buffer);
//now we need a tuple with two items to return both the integer and the string
PObject *ret_tuple = ptuple_new(2, NULL);
//it's an empty tuple, it must be filled
//let's use tuple macros
PTUPLE_SET_ITEM(ret_tuple,0,chksum_int);
PTUPLE_SET_ITEM(ret_tuple,1,chksum_str);
//done! let's return it
MAKE_RESULT(ret_tuple); //set the Python result to ret_tuple
return ERR_OK; //return ok, no exception raised
}