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_nativepass the name of the C function to be called (let’s call itcfn), the source files wherecfnimplementation resides and a list of C macros to be defined during compilation - Create the C source file containing
cfnand declarecfnusing 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:
nargsis the number of arguments passed tomy_py_functionselfis the self parameter in casemy_py_functionis a methodargsis an array of PObject*, the generic structure used by the VM to represent Python objectsresis 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_NATIVEarguments in the body of a C function callable from PythonZDEBUG(...): if the user definesZERYNTH_DEBUGinconfig.ymlequal 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_DEBUGinconfig.ymlequal 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_DEBUGinconfig.ymlequal 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_DEBUGinconfig.ymlequal 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 thenthpython argument to aint32_twithout checking the type.PYC_ARG_FLOAT(n): converts thenthpython argument to adoublewithout checking the type.PYC_ARG_BUF(n): converts thenthpython argument to auint8_t*without checking the type. Can be used to get the buffer of abytes,bytearray,short,shortarrayandstring.PYC_ARG_BUFLEN(n): return the length of thenthpython argument to aint32_twithout checking the type. The argument must be a sequence.PYC_ARG_BOOL(n): converts thenthpython argument to aint32_tfrom a boolean without checking the type.PYC_ARG_IS_NONE(n): converts thenthpython argument to a non-zeroint32_tif it isNone.PYC_CHECK_NUM_ARGS(n): raiseTypeErrorif the arguments passed from Python are notn.PYC_CHECK_ARG_INTEGER(n): raiseTypeErrorif thentharguments passed from Python is not of integer type.PYC_CHECK_ARG_FLOAT(n): raiseTypeErrorif thentharguments passed from Python is not of float type.PYC_CHECK_ARG_BOOL(n): raiseTypeErrorif thentharguments passed from Python is not of bool type.PYC_CHECK_ARG_BUFFER(n): raiseTypeErrorif thentharguments passed from Python is not usable withPYC_ARG_BUF.MAKE_RESULT(obj): set the Python result of the function toobj.objmust 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,memchrmalloc,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
}