Integration

To make SNAPpy and C code play nicely together, you need to take a few steps to foster communication between your C code and your SNAPpy code. The SNAPbuild application needs to know what you’re trying to accomplish and how all the parts fit together, and a few simple additions to your code provide this information.

Importing C Files

For each C code file you want to use, you must run the import_c("filename.c") function in your SNAPpy file. If you have several C source files, you can repeat this command as many times as necessary to get them all.

In addition to the C code filename, there are three optional parameters. The full API is:

import_c(filename[, include[, library[, args]]])

The C file location and filename can be specified as an absolute or relative path. If a relative path is specified, it should be relative to the location of the SNAPpy script that imports it, typically your snappyImages directory.

The include parameter can be specified as a keyword argument. It should be a list of include paths (as strings), indicating where the compiler should look for files that are included by using the #include keyword. Paths can be relative or absolute.

The library parameter can be specified as a keyword argument. Use this to specify the C library (as a filename string) AVR links against at link time. (You only need to specify it once, even if you have multiple C files imported.) The default (and recommended) library is cl3sec_mul.r90. Change this parameter at your own risk, as it is a good way to ensure that what you compile will not be compatible with the module.

The args parameter can be specified as a keyword argument. It should be a list of compiler arguments you wish to apply: args=["-Dfoo", "-Dsomethingelse"]

The import_c() function is not a function that can be called at run-time. It is meaningful only in the context of building the .spy file.

Including SNAPpy Definitions

Similarly, you must provide an include directive in your C files: #include "snappy.h". This header file is generated by SNAPbuild when you kick off the creation of your .spy file, so don’t worry that it doesn’t yet exist when you are writing your C code. It contains type definitions and information about where things are located in memory and in flash for the C code to access.

Associating SNAPpy Function Stubs

You must define a function stub in your SNAPpy script for any C function that you want to be able to call from your script or from another device. This step instructs SNAPbuild to create the wrapper code for the passing of parameters and return values when invoking the C code. The code stub also provides a place in your SNAPpy code to add a docstring and any relevant comments that clarify what the C function is expected to accomplish. The docstring associated with the function stub will appear in Portal when you mouse over the function name in the Node Info pane.

To create a stub appropriate for a C function named my_C_function that takes three parameters, three lines of code are sufficient:

@c_function()
def my_C_function(parameter1, parameter2, parameter3):
    pass

The first line, the @c_function() decorator, notifies SNAPbuild that the following function is a stub referring to C code. You can add a string parameter to the decorator to have the decorated SNAPpy function reference a C function with a different name. For example, @c_function("the_function_in_C") would make it so a call in your SNAPpy script (or by RPC) to my_C_function would invoke the the_function_in_C function defined in your C code.

The second line is just the function declaration. The sample above would pass three parameters, but your function can pass zero or more.

The final line, pass, makes the function syntactically valid, preventing errors from anything that reviews your code. If you were to put other code into the function, it would be ignored and discarded when SNAPbuild creates the .spy file.

While these three lines are sufficient, it is good coding practice to include a docstring describing the function’s purpose, and a comment or two about the location of the C code to make your script easier to maintain.

Specifying the API

Python (and thus SNAPpy) is a very loosely typed language, while C is not. In order for SNAPbuild to know exactly what it is dealing with when passing parameters and return values, you need to specify the expected interface between the SNAPpy function stub and the C function it invokes. You can do this in any combination of three ways.

The first option, and the one that takes precedence if you were to use more than one method, is to use api as a named parameter in the @c_function() decorator in your SNAPpy script, providing a list starting with the expected return type and continuing with the types of all passed parameters:

@c_function(api=["none", "int", "bool", "str"])
def f(i, b, s):
    pass

This function is defined as passing three parameters, an integer, a Boolean, and a string, to a void function that returns nothing in C.

The second and third options use comments in your C code file. You can provide inline comments with each C function you wish to access, each time including API: as a comment header:

// API:
// none f(int, bool, str)
void f(int16_t i, bool b, uint8_t * s) {
    // ...
}

/*
API:
int g(int)
*/
int g(int16_t n) {
    // ...
}

This method works perfectly well whether you comment each line with a pair of slashes or use a /* */ block comment structure.

You can also combine multiple API declarations into one comment with just one API: comment header:

/*
API:
none f(int, bool, str)
int g(int)
*/
void f(int16_t i, bool b, uint8_t * s) {
    // ...
}

int g(int16_t n) {
    // ...
}

In each case, whether the API is specified in the SNAPpy code or in the C code, there are specific names you must use for each of the possible types. (We’ve shown them mapped to the C types appropriate for passing the incoming data in our examples. The int16_t and uint8_t types are defined in the snappy.h header file as appropriate types to receive the values passed in from SNAPpy scripts. You can use other appropriate types if you prefer.) The valid types are:

SNAPpy/API type Appropriate C type
none void
bool bool
int int16_t
const str uint8_t DECL_GEN *
str uint8_t *
none
This is only valid for return value types on void functions in C, where the SNAPpy script will return None. (You would not normally pass a None value in to a C function.)
bool
True or False in SNAPpy, this is handled as 0 for False or anything else (typically 1) for True in the C processing. These values can be used as passed parameters or return values.
int
This is a 16-bit signed integer, suitable for passed parameters or return values. The int16_t type is defined in the snappy.h header file. You can use a different type in your C functions if you prefer, but be sure that it will be appropriate for working with the provided data type.
const str
This is (a pointer to) a SNAPpy string, either in RAM or in flash, that your C function may not modify. It is valid only as a parameter. A constant (or not-yet-modified-by-SNAPpy) string in your script or a non-modified value loaded from an NV parameter, which exists in flash rather than RAM, can be passed this way.
str

This is (a pointer to) a SNAPpy string in RAM, which may be modified by the C function. Within your C code you can use the GET_STR_LEN and SET_STR_LEN macros to determine how long your string is. Strings are not null-terminated; \\x00 is a valid character within your strings.

You cannot return a string from a C function, and you cannot allocate a new string buffer from within your C function. However, strings are modified in place in RAM, so changes made to the string by your C function will still be in the string when control returns to your SNAPpy code.

Remember that strings in SNAPpy come from a set pool of string buffers in fixed lengths. You cannot arbitrarily change the length of a string and be certain that your change is not going to cause trouble, unless you are certain of the size of the string buffer in use for your string. While you can be confident that a string known to be 17 characters long can be (carefully) expanded to up to 126 characters (because it must be using at least a medium string buffer), you cannot be confident that a string of 16 characters has any room to grow (as it could be in a small string buffer).

If you need to be able to safely extend your strings, be sure that the string buffer in use is large enough to accommodate the new size.

This string must also be one that has been assigned a SNAPpy string buffer. If a string has been passed as a parameter, it should be copied to a new variable before being sent as a parameter: newString = passedString[:]

Note

Until you modify a string in your SNAPpy script (or unless you receive it from some external interface, such as a parameter from another device or as serial input), the string value exists only in flash, rather than RAM. This is also true if you assign a string value from an NV parameter to a variable without modifying it in the variable.

Thus, name = loadNvParam(8) means that name will hold the device’s Name value, but that information is still “locked” in flash. If you wish to be able to edit the string in your C code, you can have SNAPpy build a new string in RAM as you load it, like this: name = loadNvParam(8)[:]. This is consistent with SNAPpy modifications of Byte Lists in your code and is discussed in the SNAP Users Guide.

If you get carried away and specify your API in more than one location, SNAPbuild will use the definition in the SNAPpy code, if it is available. If not, it will use the first definition that it encounters, ignoring any subsequent redefinitions. If you include a definition in more than one C code file, the files will be checked in the order they are specified in your SNAPpy file.

If you end up calling your function stub with the wrong kinds of variables (something that SNAPpy and Python accept without issue), the C wrappers generated by SNAPbuild do not invoke the C code but simply return None.