Coding guide impl

From GNU Radio
Revision as of 16:17, 18 March 2017 by Wgebers (talk | contribs)
Jump to navigation Jump to search

Coding Guide

THIS PAGE IS A WORK IN PROGRESS TO DOCUMENT THE NEW CODING STYLE AS OF v3.7

This document specifies the coding style and structure of blocks in GNU Radio as of version 3.7. The blocks are structured under top-level component (TLC) directories that combine similar blocks/algorithms (e.g., all filtering blocks are found in gr-filter). For purposes of explanation here, we will work with the generic TLC called foo and the block bar. Data types for member variables are generically called dtype.

Directory Structure

  • apps: any full-fledged applications specific to the component
  • doc: place for extra documentation, including Doxygen .dox files to describe the component.
  • examples: example code to demonstrate usage of component blocks and algorithms.
  • grc: GNU Radio Companion files and block tree.
  • include: public header files
    • include/foo: actual location of public header files. Headers in included using #include <foo/bar.h>.
  • lib: location of the implementation source and private header files (bar_impl.h and bar_impl.cc, generally).
  • swig: location of SWIG interface file. We use a simplified structure that only includes the public header file for SWIGing.

Namespaces

Components are described under the namespace gr::foo

In Python, each TLC is a component under the gnuradio module. So to get access to blocks in foo, do:

from gnuradio import foo

Code Format

We want to try and make a consistent look and feel to the code in GNU Radio. Here are a few things to keep in mind while writing code.

All lines of code should not exceed 70 characters on a width. Often, there is a logical break in the line of code that allows it to easily span two lines, such as between arguments to the function. Sometimes not, though, so make your best judgment here. The reason for this is to make the code format easily readable in both an editor and when printed out.

As stated elsewhere, no CamelCase. All distinct words in a function or variable name should use underscores:

int foo_bar

In the header file, functions should all be written on the same line, up to the 70-character limit.

int bar(int arg1, int arg2);

In the C++ implementation file, the function should have its return data type indicated on a separate line.

int foo::bar(int arg1, int arg2)

Curly brace convention is as follows. After a function name, the curly brace should follow on the next line, and the ending curly brace should be on its own line. Inside of a function, such as for an if statement or for loop, the beginning curly brace should come after the statement, while the end curly brace should be on its own line. Note the spacing used in this example, too.

int
foo::bar(int arg1, int arg2)
{
  if(arg1 == 0) {
     ;
  }
  else {
    ;
  }
  return 0;
}

Although C++ allows single-line statements after a for or if statement to exist without curly braces surrounding them, it is recommended to always use curly braces. This may help prevent problems in the future if another line is added to the code block.

Block naming conventions

Blocks should be given a name that indicates its purpose as well as its input and output data types. If a block lives in the GNU Radio core and performs the function FOO that takes complex in and produces floating points outputs, it's name should be:

foo_cf

This boils down to:

<name>_<input><ouput>

This lives in the namespace gr::<module>.

The name indicates the function of the block, such as "add" or "fir_filter".

The input indicates the input type while output indicates the output type. Currently, the types are:

  • c: complex (32-bit floats for both I and Q)
  • f: float (32-bit single precision IEEE 754 style)
  • i: integer (32-bit signed integers)
  • s: short (16-bit signed integers)
  • b: bytes (8-bit signed integers)

There is no differentiation between bytes and bits, even though some blocks use unpacked format where a single bit is represented in a byte. If this is the case, it should be made clear in the documentation for the block.

GNU Radio also has a concept of vectors, where the data into or out of the block are vectors of items. The "v" is then used to indicate if this is the case. A block that uses vectors should indicate it by the type:

<vector in><input><vector out><output>

So a block that takes vectors in but streams samples out would look like vcc, but a block that does vectors for both input and output will look like vcvc.

NOTE: this is a new convention and older blocks that use vectors do not follow this. They generally only indicate that vectors are used somewhere by putting a single "v" in front of the data type indicator.

Block Coding Guidelines

All GNU Radio blocks have a structure and style that we try to keep consistent. There are also a number of concepts and general practices to understand when building a new block. We have a separate page describing the block coding guidelines:

Standard command line options

When writing programs that are executable from the command line, please follow these guidelines for command line argument names (short and long) and types of the arguments. We list them below using the Python optparse syntax. In general, the default value should be coded into the help string using the "... [default=%default]" syntax.

The following shows the boilerplate text for using the optparse Python module (see http://docs.python.org/library/optparse.html).

from optparse import OptionParser
….
def :
    usage = ””
    desc = “”
    parser = OptionParser(usage=usage, description=desc, option_class=eng_option)
    parser.add_option("-a", "--args", type="string", default="",
                          help="UHD device address args , [default=%default]")
    ….
    (options, args) = parser.parse_args()

Standard options names by parameter

Whenever you want an integer, use the "intx" type. This allows the user to input decimal, hex or octal numbers. E.g., 10, 012, 0xa.

Whenever you want a float, use the "eng_float" type. This allows the user to input numbers with SI suffixes. E.g, 10000, 10k, 10M, 10m, 92.1M

If your program allows the user to specify values for any of the following parameters, please use these options to specify them:

  • To specify a frequency (typically an RF center frequency) use:

add_option("-f", "--freq", type="eng_float", default=<your-default-here>,

            help="set frequency to FREQ [default=%default]")
  • To specify a sample rate use:

add_option("-s", "--samp-rate", type="eng_float", default=<your-default-here>,

            help="set sample rate to DECIM [default=%default]")
  • To specify a gain setting use:

add_option("-g", "--gain", type="eng_float", default=<your-default-here>,

        help="set gain in dB [default=%default]")
  • To specify the number of channels of something use:

add_option("-n", "--nchannels", type="intx", default=1,

            help="specify number of channels [default=%default]")
  • To specify an output filename use:

add_option("-o", "--output-filename", type="string", default=<your-default-here>,

            help="specify output-filename [default=%default]")
  • If your application has a verbose option, use:

add_option('-v', '--verbose', action="store_true", default=False,

        help="verbose output")

Example: Audio source

add_option("-I", "--audio-input", type="string", default="",

            help="pcm input device name.  E.g., hw:0,0 or /dev/dsp")

The default must be "". This allows an audio module-dependent default to be specified in the user preferences file.

Example: Audio sink

add_option("-O", "--audio-output", type="string", default="",

            help="pcm output device name.  E.g., hw:0,0 or /dev/dsp")

The default must be "". This allows an audio module-dependent default to be specified in the user preferences file.

C++

Global names

All globally visible names (types, functions, variables, consts, etc) shall begin with a "package prefix", followed by an '_'.

Package prefixes

We have moved away from the concept of package prefixes and use namespaces instead. See the BlockCodingGuide page for description and examples of this. For older code, we specified what kind of block it was using a package prefix. For example, we had:

gr_: Almost everything

gri_: Implementation primitives. Sometimes we have both a gr_<foo> and a gri_<foo>. In that case, gr_<foo> would be derived from gr_block and gri_<foo> would be the low level guts of the function.

qa_: Quality Assurance. Test code.

Now, a block that used to be called gr_foo that is a general block would now be located in the top-level component gr-block. The header file would be in gr-block/include/block/foo.h. It would then be in the namespace gr::block.

Implementation files are just named for what they are. For example, the fft_vcc_fftw block uses the implementation of the FFT found in gr::fft::fft. Sometimes, when there is a name collisions, we put these under the namespace gr::foo::kernel. See the gr-filter blocks to create FIR filters.

Naming conventions

  • Functions and variable are underscore_separated_words (No CamelCase).
  • All data members of a class use a "d_" prefix to them, such as d_bar. They should also be private unless there is a very good reason not to.

All data member should have a settable and gettable accessor function of the form:

  • void set_variable(type var);
  • type variable();

These accessors should perform any required range checking and throw a "std::out_of_range" exceptions when not met. Other conditions that are not met can throw a "std::invalid_argument" exception.

The big win is when you're staring at a block of code, it's obvious which of the things being assigned to persist outside of the block. This also keeps you from having to be creative with parameter names for methods and constructors. You just use the same name as the instance variable, without the d_.

Example:

class gr_wonderfulness {
  std::string   d_name;
  double    d_wonderfulness_factor;

public:
  wonderfulness(std::string name, double wonderfulness_factor)
    : d_name(name), d_wonderfulness_factor(wonderfulness_factor)
  {
    ...
  }
  ...
};
  • All class static data members shall begin with “s_” as in “s_<bar>”.

Accessor Functions

All signal processing blocks should have accessors to get and set values. For parameter param, the get function is param() and the set function is set_param(type new_value) for any variables that can be changed at runtime.

Storage management

Strongly consider using the boost smart pointer templates, scoped_ptr and shared_ptr. scoped_ptr should be used for locals that contain pointers to objects that we need to delete when we exit the current scope. shared_ptr implements transparent reference counting and is a major win. You never have to worry about calling delete. The right thing happens.

See http://www.boost.org/libs/smart_ptr/smart_ptr.htm

Exceptions

Use the following exceptions:

  • std::runtime_error: when a runtime error has occurred
  • std::out_of_range: when a variable to be set is not within a given range
  • std::invalid_argument: when an argument is set to an improper value

White Space Removal

While this might sound a little pedantic, we encourage everyone to remove trailing white space on any lines edited as well as extraneous lines at the end of the file. This helps keep our code clean, but more importantly, it helps keep our git logs clean. Removal of trailing white spaces can be done by asking your favorite editor to do it for you on save. See the Setup Recommendations for Standard Editors guide at the end here for hints about how to get your editor to help you remove any trailing white space.

You can use a program written for GNU Radio to remove any trailing white spaces from all files in your current directory tree. You can find this file in <source tree>/dtools/bin/remove-whitespace. It's a sed script that does the following:

sed -i -e 's/\s\+$//g' $(find . -type f | grep -v '.git' | grep -v 'png' | grep -v 'eps')

Notice that it ignores all .git, .png, and .eps files to preserve their standard formatting.

We infrequently run this script on the GNU Radio source tree, so depending on when you've last updated the source, you may or may not have a lot of white space that's snuck into the code.

Unit testing

Build unit tests for everything non-trivial and run them after every change. Check out Extreme Programming: http://c2.com/cgi/wiki?ExtremeProgrammingRoadmap

Unit tests should also be written for all examples.

Unit tests can be written in Python or in C++. If possible, use Python unit tests (e.g., to test the output of signal processing blocks).

C++ unit tests

For C++, we use cppunit, which has some issues, we have it nicely integrated into the build system. http://cppunit.sf.net

Currently each top-level component <tlc> contains files qa_<tlc>.{h,cc} in the lib directory that bring together all the qa_<foo> test suites in the directory.

Python unit tests

We use the standard unittest package for unit testing of Python code.
http://docs.python.org/library/unittest.html

We have added our own interface called gr_unittest to the unittest to overload and add some functions to make it simpler to test GNU Radio blocks. The Python QA functions are located in the top-level components python directory. They are all named “qa_<foo>.py”. Any Python file with the “qa_” prefix will be automatically tested.

A general QA file looks like:

from gnuradio import gr, gr_unittest
import MODULE_swig as MODULE

class test_foo(gr_unittest.TestCase):
    def setUp (self):
        self.tb = gr.top_block()

    def tearDown (self):
        self.tb = None

    def test_001(self):
        source_data = […]
        expected_data = […]

        src = gr.vector_source_X(source_data)
        head = gr.head(, )
        op = 
        snk = gr.vector_sink_X()

        self.tb.connect(src, head, op, snk)
        self.tb.run()

        result_data = snk.data()
        self.assertXXX(expected_data, result_data, )

if __name__ == '__main__':
    gr_unittest.run(test_foo, "test_foo.xml")

The self.assertXXX performs the test between the expected and actual data up. There are many assertions that are already part of the Python unittest module and gr_unittest adds many more, specifically to allow us to test different data types with a set amount of precision that is more appropriate for signal processing tests. These include:

  • assertComplexAlmostEqual(self, first, second, places=7, msg=None)
  • assertComplexAlmostEqual2(self, ref, x, abs_eps=1e-12, rel_eps=1e-6, msg=None)
  • assertComplexTuplesAlmostEqual(self, a, b, places=7, msg=None)
  • assertComplexTuplesAlmostEqual2(self, ref, x, abs_eps=1e-12, rel_eps=1e-6, msg=None)
  • assertFloatTuplesAlmostEqual(self, a, b, places=7, msg=None)
  • assertFloatTuplesAlmostEqual2(self, ref, x, abs_eps=1e-12, rel_eps=1e-6, msg=None)

See the definitions of these in “gnuradio-runtime/python/gnuradio/gr_unittest.py” in the source code for details.

Setup Recommendations for Standard Editors

Here are a few hints for some of our favorite editors to help you get your code formatted properly to work with our standards. Most of it's straight-forward, and these help make sure spacing and white space issues are kept consistent.

Emacs

(setq-default show-trailing-whitespace t)                ;; show trailing white space as color def. next
(set-face-background 'trailing-whitespace "#72706F")
(setq-default show-tabs t)                               ;; show tabs; should be replaced by spaces
(setq-default indent-tabs-mode nil)                      ;; use spaces, not tabs, for indenting
(add-hook 'before-save-hook 'delete-trailing-whitespace) ;; remove trailing white space on save

ViM

" You won't need every setting in this list. The below command is the one that will be most generally 
" useful to everyone.
autocmd BufWritePre * :%s/\s\+$//e                       " Automatically remove trailing whitespace on save

" In addition to having trailing whitespace automatically removed when you save a file, you can also 
" map a key sequence (e.g., `-w`) for the purpose. This is useful for remove trailing whitespace 
" without saving, but while editing.
nnoremap w mz:%s/\s\+$//:let @/=''`z

" If you would also like to visualize whitespace, you can try this out
set listchars=tab:▸\ ,extends:❯,precedes:❮,trail:·      " Show special characters for tabs, trailing spaces, and broken indents

" Alternatively, you can show whitespace characters only while in Insert mode. For example:
augroup trailing
    au!
    au InsertEnter * :set listchars+=tab:▸\ ,extends:❯,precedes:❮,trail:·,eol:¬
    au InsertLeave * :set listchars-=tab:▸\ ,extends:❯,precedes:❮,trail:·,eol:¬,
augroup END