Skip to content

Making declarations

Pavlo Tymoshenko edited this page Dec 26, 2021 · 8 revisions

Declaration is a single source of information about things included into your library.

How to make declarations?

You should enclose your declarations in /* */ code block. Look for declaration examples below.

All declarations consists of units, which is some keyword (like @member) with a list of values separated by space. The first unit is always define declaration type. Units can be:

  • singular: only one unit with such name can be presented in the declaration (e.g. return type of the function),
  • plural: several units with such name can be used (e.g. members of the struct).

Which types are supported?

Following declaration types are supported: functions, structs, enums, flags, variables, typedefs.

@function declaration

This declaration defines a function placed. It supports the following singular units:

  • @return <return type>: defines return type of the function.

Plural units:

  • @argument <argument type> (optional): defines type of the single argument. All arguments should be placed in the same order they appear in C function declaration.

Declaration example:

/* @function function_name
 * @return long
 * @argument int
 * @argument char
 */
long function_name(int number, char character);

Declared functions can be accessed from the Loader instance by its name. Example:

lib = annotatec.Loader("lib.so", ["source1.h"])
lib.function_name(int_variable, char_variable)

@struct declaration

This declaration defines a structure and its members as a type. Only one plural unit are available:

  • @member <member type> <member name>: defines type and name of the structure's member.

Declaration example:

/* @struct my_structure
 * @member int number
 * @member char character
 */
typedef struct {int number, char character} my_structure_t;

Declared struct can be used as a type. Example:

inited_struct = lib.my_structure(
    0,    # int number
    'a'   # char character
)

Now this object can be passed to any function which takes such structure. For example, you have:

/* @function take_struct
 * @return void
 * @argument my_structure
 */
void take_struct(my_structure_t *struct) { ; }

This function can be called by

lib.take_struct(ctypes.pointer(inited_struct))

@enum declaration

This declaration allows you to use enum objects in your Python. Singluar units:

  • @type <enum type> (optional): enumerators is usually int. But just in case you need different type, your enumerator value can be casted to anything you need.

Plural units:

  • @member <name> <value>: defines a single enumerator. <value> can be anything executable to number. For example: 5, 0b001, (1 << 3) or 0xA. If you need to use spaces, be sure your value is bracket-enclosed (e.g. (1 + 2)).

Consider the following code:

/* @enum mode
 * @member mode1                        1
 * @member mode3_long_name              3
 * @member mode8_complicated_expression (1 << (3 ^ 5))
 */
typedef enum {
    mode1                        = 1,
    mode3_long_name              = 3,
    mode8_complicated_expression = (1 << (3 ^ 5))
} mode_t;

/* @function enum_function
 * @return int
 * @argument mode
 */
int enum_function(mode_t mode) {
    if (mode == mode1)                        return 10;
    else
    if (mode == mode3_long_name)              return 30;
    else
    if (mode == mode8_complicated_expression) return 80;
}

When compiled into dynamic library and parsed with annotatec, it can be used like this:

lib.enum_function(lib.mode.mode1)  # 10
lib.enum_function(
    lib.mode.mode3_long_name)  # 30
lib.enum_function(
    lib.mode.mode8_complicated_expression)  # 80

@flags declaration

Flags can combine several options (mostly boolean ones) inside one variable. Flags can be combined using OR operator (|) or addition (+): flag1 | flag2, flag1 + flag2.

Imagine you're making some function which opens an Internet connection and may take the following options:

  • RETRY_ON_ERROR: if turned on, connect attemption will be made several times,
  • RECONNECT_ON_ERROR: if turned on, connection will be reestablished in case of disconnection,
  • USE_SECURE: if turned on, some sort of secure connection will be used.

In order to encage states of these options into one variable, their values should not overlap at the bit-level (just like in bitmask).

Singular units:

  • @type <type name>: defines type used for flag variable.

Plural units:

  • @member <flag name> <flag value>: defines name and the value of the flag. <flag value> can be something executable just like in the @enum declaration.

Consider the following C code:

/* @flags connection_mode
 * @member RETRY_ON_ERROR
 * @member RECONNECT_ON_ERROR
 * @member USE_SECURE
 */
typedef char connection_mode_t;
const connection_mode_t RETRY_ON_ERROR     = (1 << 0);
const connection_mode_t RECONNECT_ON_ERROR = (1 << 1);
const connection_mode_t USE_SECURE         = (1 << 2);

/* @enum connection_result
 * @member CONNECTION_OK   1
 * @member CONNECTION_FAIL 2
 */
typedef enum {OK = 1, FAIL = 2} connection_result_t;

/* @function connect
 * @return connection_result
 * @argument connection_mode
 */
connection_result_t connect(connection_mode_t mode) {
    // ... do some logic
    return fail ? FAIL : OK;
}

It can be used in the Python wrapper like so:

result = lib.connect(
    lib.connection_mode.RETRY_ON_ERROR | ilb.connection_mode.USE_SECURE)

if result == lib.connection_result.OK:
    print("success!")
elif result == lib.connection_result.FAIL:
    print("fail!")
else:
    print("I saw an UFO")

@variable declaration

This declaration allows you to use global variables defined in the dynamic library. The only unit available is singular:

  • @type <variable type>: defines type of the variable.

Consider the following example. lib.h

/* @variable ERROR_LEVEL
 * @type int
 */
extern int ERROR_LEVEL;

lib.c source:

int ERROR_LEVEL = 0;

/* @function do_something
 * @return void
 */
void do_something(void) {
    // ...some logic
    if (fail) ERROR_LEVEL = 1;
    else      ERROR_LEVEL = 0;
}

/* @function reset_error
 * @return void
 */
void reset_error(void) {
    ERROR_LEVEL = 0;
}

The code above introduces a variable which can store the code of the last error happened. It can be accessed from Python like so:

print(lib.ERROR_LEVEL)  # can be anything
lib.do_something()      # this function change the
                        # value of ERROR_LEVEL
print(lib.ERROR_LEVEL)  # "0" or "1" will be printed
lib.reset_error()       # reset ERROR_LEVEL to 0
print(lib.ERROR_LEVEL)  # "0" will be printed

By the way, such implementation is good to combine with @enum. For example:

/* @enum error
 * @member ok     0
 * @member error1 1
 * @member error2 2
 */
typedef enum {OK = 0, ERROR1 = 1, ERROR2 = 2} error_t;
/* @variable ERROR_LEVEL
 * @type error
 */
error_t ERROR_LEVEL = OK;

@typedef declaration

Such declaration allows you to create an alias for the type. Only one singular unit is allowed:

  • @from_type <type name>

Consider the following C code:

/* @typedef some_other_type
 * @from_type int
 */
typedef int some_other_type;

Such declaration allows you to use some_other_type literal in other declarations:

/* @function some_function
 * @return some_other_type
 * @argument some_other_type
 */
some_other_type some_function(some_other_type);

Now your type some_other_type is compatible with int, so you shouldn't cast your number to some_other_type before passing to the function which accepts it. But anyway, it can be done just for fun 😄

variable = lib.some_other_type(5)
lib.some_function(variable)  # but lib.some_function(5)
                             # will be fine as well