Examples

Simple Example

For our first example we’ll try something simple: a C function that returns True if the passed integer is even, else False.

Inside your snappyImages directory, create a file named even.c, with the following contents:

#include "snappy.h"

// API:
// bool is_even(int)
bool is_even(int16_t val) {
    return (val % 2) == 0;
}

Inside your snappyImages directory, create a second file named even.py, with the following contents:

import_c("even.c")

@c_function
def is_even(v):
    '''Check if a number is even'''
    pass

Now open a command window to your snappyImages directory, and execute the following command:

snapbuild –m SM220 –c 2.7.1 even.py

If you are working with a device format other than an SM220 Series, or a firmware release other than 2.7.1, modify the parameters accordingly. Because the C code is linked, it must be built for a specific module and firmware version. Using a .spy file built for one combination of module and firmware in a different environment is not likely to work and could potentially corrupt the device’s firmware.

This command will create the even.spy file in your snappyImages directory. You can now load this .spy file using SNAPtoolbelt.

Once you have the script running in the device, you invoke the is_even() function via SNAPtoolbelt. The function can just as easily be called by other functions on the node, or by any other device via RPC (or multicast).

even.py

Let’s start with the details in the SNAPpy file, even.py.

The first line of the file is a function call that tells SNAPbuild that there is a C program that needs to be included:

import_c("even.c")

The next line is a function decorator:

@c_function

This function decorator tells SNAPbuild that the SNAPpy function that follows should be ignored, and that a C function of the same name should be wrapped for inclusion instead. Note that when no parameters are supplied to the @c_function decorator, the parentheses are optional. Finally, there is the function stub in the SNAPpy file. This defines the name by which the function will be available when called by other functions in the SNAPpy script or when called by other devices.

You can name your function parameters anything that is meaningful to you in context and have as many parameters as is appropriate for your function. Parameters will be passed positionally to your C function, so there is no requirement that the parameter names match in the function signatures.

The recommended practice is to have pass (and possibly a comment regarding where the C function is defined and what it does) be the complete contents of your function stub in your SNAPpy file. As with a good docstring, this manner of documentation makes code maintenance easier for yourself or whoever comes behind you later. If you have “real” code in your function stub, it will be completely ignored (because of the function decorator) and replaced with the wrapped code from your C file.

even.c

There are also some requirements for your C code file for this effort, too. Notice that the file starts with an include directive for snappy.h, a file that doesn’t (yet) exist:

#include "snappy.h"

That header file is generated by SNAPbuild when you build the .spy file.

Next comes a comment defining the API for this function.

// API:
// bool is_even(int)

This API declaration tells SNAPbuild exactly what kinds of variables to expect, so that it can pass known entities through its wrappers to your C code. You must have an API declaration for each function you wish to have available through wrappers in your SNAP device.

An important consideration here is that you can have other functions in your C code that are not accessible from SNAPpy but that are used by the C functions that SNAPpy does invoke.

Advanced Example

We’ll provide another sample program that demonstrates several of the ways to use SNAPpy and C. Because the details related to what is going on in the code files have already been discussed, these files will not be broken down here. The code comments should be sufficient to understand what is going on and to see how things are processed.

wrap_test.py

This SNAPpy code references several functions in the associated C file and provides some SNAPpy unit testing to confirm that things work as expected.

import_c("wrap_test.c")

###########################################################################
# plus_one (no wrapper)
# This is an example of invoking a C function without specifying the API
# between the SNAPpy function and the C function.
###########################################################################

@c_function
def plus_one(v):
    """
    Expects to receive an integer (signed 16-bit), and returns that value plus
    1. Results are undefined if you pass something other than an integer.
    """

    # Associated C code in wrap_test.c as plus_one
    pass


def test_plus_one():
    v = 42
    if 43 != plus_one(v):
        return "FAIL plus_one: positive add"

    v = -42
    if -41 != plus_one(v):
        return "FAIL plus_one: negative add"

    v = -1
    if 0 != plus_one(v):
        return "FAIL plus_one: -1"

    v = 0
    if 1 != plus_one(v):
        return "FAIL plus_one: 0"

    v = 32767
    if -32768 != plus_one(v):
        return "FAIL plus_one: overflow"

    return "PASS plus_one"


###########################################################################
# minus_one (no wrapper, but renamed)
# This is an example of invoking a C function without specifying the API
# between the SNAPpy function and the C function.
# This also demonstrates the ability to have the SNAPpy and C functions
# have different names.
###########################################################################
@c_function("minus_1")
def minus_one(v):
    """
    Expects to receive an integer (signed 16-bit), and returns that value minus
    1. Results are undefined if you pass something other than an integer.
    """

    # Associated C code in wrap_test.c as minus_1
    pass


def test_minus_one():
    v = 42
    if 41 != minus_one(v):
        return "FAIL minus_one: positive subtract"

    v = -1
    if -2 != minus_one(v):
        return "FAIL minus_one: negative subtract"

    v = 0
    if -1 != minus_one(v):
        return "FAIL minus_one: zero subtract"

    v = -32768
    if 32767 != minus_one(v):
        return "FAIL minus_one: underflow"

    return "PASS minus_one"


###########################################################################
# vcount
# Note the API declaration within the function decorator.
###########################################################################
@c_function(api=["int", "const_str"])
def vcount(s):
    """
    Given a string (in either RAM or flash), returns the number of lower-case
    vowels (from the set a, e, i, o, u) in the string, as an integer.
    """

    # Associated C code in wrap_test.c as vcount
    pass


def test_vcount():
    rv = vcount("abcdefghij")
    if rv != 3:
        return "FAIL vcount: const string " + str(rv)

    saveNvParam(130, "zqroeoebut")
    rv = vcount(loadNvParam(130))
    if rv != 5:
        return "FAIL vcount: nvparam string " + str(rv)

    return "PASS vcount"


###########################################################################
# uppercase
# Note that the API declaration is in the C file for this function.
###########################################################################
@c_function
def uppercase(s):
    """
    Converts a given string to uppercase. Returns an integer of the number
    of characters that were case-converted. The string is modified in place.
    """

    # Associated C code in wrap_test.c as uppercase
    pass


def test_uppercase():
    s = 'A'

    i = 5
    while i > 0:
        i -= 1

    s += 'b'

    rv = uppercase(s)
    if rv != 5:
        return "FAIL uppercase: bad conversion count: " + str(rv)

    rv = uppercase("abcdefg")
    if type(rv) != 0:
        return "FAIL uppercase: didn't catch const str"

    return "PASS uppercase"


###########################################################################
# call count (wrapper, renamed, but with same name)
# Note that we can explicitly specify a C function name (though we do not
# change it this time), and we specify the API in the function decorator.
###########################################################################
@c_function("f", api=["none", "int", "bool", "str", "const_str"])
def f(i, b, s, cs):
    """
    This function requires an integer, a boolean, a string, and a constant
    string as parameters, returning None. The parameters are not modified by
    the function, but the function increments an internal counter on each
    call. You can check the call count using the get_count() function.
    """

    # Associated C code in wrap_test.c as f
    pass


###########################################################################
# get call count
# Note that the SNAPpy function stub does not have to have the same name
# as the C function it invokes. The API declaration indicates that the C
# code returns an integer and takes no parameters.
###########################################################################
@c_function("get_count", api=["int"])
def get_f_count():
    """
    This function returns a count of the number of times the f() function has
    been called.
    """

    # Associated C code in wrap_test.c as get_count
    pass


###########################################################################
# reset call count (wrapper, renamed, but with same name)
###########################################################################
@c_function("reset_count", api=["none"])
def reset_count():
    """
    Reset the count of the number of times f() has been called.
    """

    # Associated C code in wrap_test.c as reset_count
    pass


def test_callcount():
    reset_count()

    rv = get_count()
    if rv != 0:
        return "FAIL callcount: bad initial rv"

    # Make a dynamic string for type check testing
    s = 'A'
    i = 3
    while i > 0:
        i -= 1
    s += 'b'

    # Should work and call the underlying f
    f(5, True, s, "abcd")
    rv = get_count()
    if rv != 1:
        return "FAIL callcount: step 1: " + str(rv)

    # Should work and call the underlying f
    f(5, True, s, s)
    rv = get_count()
    if rv != 2:
        return "FAIL callcount: step 2: " + str(rv)

    # Should fail and not call the underlying f
    f(5, True, "abcd", "defg")
    rv = get_count()
    if rv != 2:
        return "FAIL callcount: step 3: " + str(rv)

    # Should fail and not call the underlying f
    f(5, True, 6, "defg")
    rv = get_count()
    if rv != 2:
        return "FAIL callcount: step 4: " + str(rv)

    # Should fail and not call the underlying f
    f(5, True, True, "defg")
    rv = get_count()
    if rv != 2:
        return "FAIL callcount: step 5: " + str(rv)

    # Should fail and not call the underlying f
    q = None
    f(5, True, q, "defg")
    rv = get_count()
    if rv != 2:
        return "FAIL callcount: step 6: " + str(rv)

    # Should fail and not call the underlying f
    f(False, True, s, s)
    rv = get_count()
    if rv != 2:
        return "FAIL callcount: step 7: " + str(rv)

    # Should fail and not call the underlying f
    f(5, True, s)
    rv = get_count()
    if rv != 2:
        return "FAIL callcount: step 8: " + str(rv)
    return "PASS callcount"


###########################################################################
# fill_data1
# This function potentially changes the length of the passed string, so
# there is risk of overflowing the string buffer.
###########################################################################
@c_function(api=["none", "str", "int"])
def fill_data1(s, count):
    """
    Take a string s and fill it to length count, with 'w' at each character.
    This function returns None.

    Note: This is potentially dangerous. If you pass a count value longer
    than the string buffer in use for the passed string, you can overflow
    into other data in RAM.
    """

    # Associated C code in wrap_test.c as fill_data1
    pass


def test_fill_data1():
    s = ''

    i = 0
    # Construct a string as large as SNAPpy can handle to pass in.
    while i < 255:
        s += ' '

    i += 1

    fill_data1(s, 10)
    if s != "wwwwwwwwww":
        return "FAIL fill_data1: " + s

    return "PASS fill_data1"


###########################################################################
# fill_data2 (with renamed function)
# This function does not change the length of the passed string, so there
# should be no risk of overflow.
###########################################################################
@c_function("fill_data_two", api=["none", "str"])
def fill_data2(s):
    """
    Take a string s and replace every character in it with 'q'.
    This function returns None.
    """

    pass


def test_fill_data2():
    s = ''

    i = 0
    while i < 20:
        s += ' '

    i += 1

    fill_data2(s)
    if s != "qqqqqqqqqqqqqqqqqqqq":
        return "FAIL fill_data2: " + s

    return "PASS fill_data2"

wrap_test.c

The C code associated with the above SNAPpy script also has comments inline to offer explanation about what is going on.

#include "snappy.h"

// This function has no defined API, so the function prototype must match what
// we have here, and the function itself must extract the parameters from the
// Python stack and put the return value there.
void plus_one(uint8_t nargs, py_obj *stack, uint8_t *unused) {
    int16_t v = (int16_t)stack[PARAM_INDEX(0, nargs)].value;
    stack[RV_INDEX(nargs)].type_code = TC_INT;
    stack[RV_INDEX(nargs)].value = v + 1;
}

// This function has no defined API, so the function prototype must match what
// we have here, and the function itself must extract the parameters from the
// Python stack and put the return value there.
void minus_1(uint8_t nargs, py_obj *stack, uint8_t *unused) {
    int16_t v = (int16_t)stack[PARAM_INDEX(0, nargs)].value;
    stack[RV_INDEX(nargs)].type_code = TC_INT;
    stack[RV_INDEX(nargs)].value = v - 1;
}

// API:
// int vcount(const_str)
// Associated with the vcount() function in the wrap_test.py SNAPpy script.
int16_t vcount(uint8_t DECL_GEN * s) {
    int16_t cnt = 0;
    uint8_t i, j = 0;
    uint8_t lookup[] = "aeiou";
    uint8_t len = GET_STR_LEN(s);

    for(i = 0; i < len; ++i) {
        for(j = 0; j < 5; ++j) {
            if(s[i] == lookup[j]) {
                cnt++;
                break;
            }
        }
    }

    return cnt;
}

/*
* API:
* int uppercase(str)
* Associated with the uppercase() function in the wrap_test.py SNAPpy script.
*/
int16_t uppercase(uint8_t *s) {
    int16_t mod_cnt = 0;
    uint8_t i = 0;
    uint8_t len = GET_STR_LEN(s);

    for(i = 0; i < len; ++i) {
        if(s[i] >= 'a' && s[i] <= 'z') {
            s[i] = 'A' + (s[i] - 'a');
            mod_cnt++;
        }
    }

    return mod_cnt;
}

// This two-byte integer, declared as static RAM, consumes a large string (255-bytes,
// plus 2 bytes of overhead) in the SNAP world. If other static RAM were declared
// elsewhere in included C code, it would pull from the same 257-byte RAM space.
static int16_t call_count = 0;

// Associated with the f() function in the wrap_test.py SNAPpy script.
void f(int16_t i, bool b, uint8_t * s, uint8_t DECL_GEN * cs) {
    call_count++;
}

// Associated with the get_f_count() function in the wrap_test.py SNAPpy script.
int16_t get_count() {
    return call_count;
}

// Associated with the reset_count() function in the wrap_test.py SNAPpy script.
void reset_count() {
    call_count = 0;
}

// Associated with the fill_data1() function in the wrap_test.py SNAPpy script.
void fill_data1(uint8_t *s, int16_t count) {
    int16_t i;

    for(i = 0; i < count; i++) {
        s[i] = 'w';
    }

    SET_STR_LEN(s, count);
}

// Associated with the fill_data2() function in the wrap_test.py SNAPpy script.
void fill_data_two(uint8_t *s) {
    int16_t i;
    uint8_t len = GET_STR_LEN(s);

    for(i = 0; i < len; i++) {
        s[i] = 'q';
    }
}