Converting a Python GNU Radio block into C++ is very doable, but it’s not a mechanical translation — you’re moving from a runtime-defined block to a compiled, typed one that plugs into GNU Radio’s C++ scheduler.
Below is the clean, modern GNU Radio (3.9–3.10+) way, with emphasis on signal-processing blocks rather than message-only helpers.
Big picture: what actually changes
| Python block | C++ block |
gr.sync_block / basic_block | gr::sync_block / gr::block |
| Dynamic typing | Static typing |
| Python work() | C++ work() |
Installed via .py | Built via CMake |
| Easy iteration | Much faster execution |
You normally do not “convert” line-by-line — you re-implement the algorithm.
Step 1: Identify what kind of block you wrote
Look at your Python class inheritance:
gr.sync_block→ C++ sync_blockgr.decim_block→ C++ decim_blockgr.interp_block→ C++ interp_blockgr.basic_block→ C++ block
Most custom DSP blocks map cleanly to sync_block.
Step 2: Create a new C++ OOT module (recommended)
Do not edit GNU Radio core.
gr_modtool newmod mymodule
cd gr-mymodule
Create your block:
gr_modtool add -t sync my_block
This generates:
- Header (
include/…/my_block.h) - Implementation (
lib/my_block_impl.cc) - Python bindings
- CMake glue
Step 3: Understand the generated C++ skeleton
Header (my_block.h)
class MYMODULE_API my_block : virtual public gr::sync_block
{
public:
typedef std::shared_ptr<my_block> sptr;
static sptr make(int param);
};
Implementation (my_block_impl.cc)
my_block::sptr
my_block::make(int param)
{
return gnuradio::make_block_sptr<my_block_impl>(param);
}
Step 4: Translate the Python __init__
Python
gr.sync_block.__init__(
self,
name="my_block",
in_sig=[np.float32],
out_sig=[np.float32],
)
self.gain = gain
C++
my_block_impl::my_block_impl(float gain)
: gr::sync_block(
"my_block",
gr::io_signature::make(1, 1, sizeof(float)),
gr::io_signature::make(1, 1, sizeof(float))),
d_gain(gain)
{
}
Member variables go in the class private section:
float d_gain;
Step 5: Translate work()
Python
def work(self, input_items, output_items):
out = output_items[0]
inp = input_items[0]
out[:] = inp * self.gain
return len(out)
C++
int
my_block_impl::work(int noutput_items,
gr_vector_const_void_star& input_items,
gr_vector_void_star& output_items)
{
const float* in = (const float*)input_items[0];
float* out = (float*)output_items[0];
for (int i = 0; i < noutput_items; i++)
out[i] = in[i] * d_gain;
return noutput_items;
}
Step 6: Handle state, history, and tags
Python equivalents map cleanly:
| Python | C++ |
self.set_history(n) | set_history(n) |
self.add_item_tag() | add_item_tag() |
self.get_tags_in_range() | get_tags_in_range() |
C++ tags example:
add_item_tag(0, nitems_written(0),
pmt::string_to_symbol("key"),
pmt::from_double(value));
Step 7: Build and install
mkdir build && cd build
cmake ..
make -j
sudo make install
sudo ldconfig
Your block will now appear in:
- GNU Radio Companion
- Python via
import mymodule
Step 8: Test against your Python block
Before deleting the Python version:
- Feed identical test vectors
- Compare outputs numerically
- Verify tag behavior
- Check threading assumptions
Common traps (important)
- Python slicing ≠ C++ You must loop or use
volk_*kernels. - Scheduler differences Don’t assume fixed buffer sizes.
- Memory alignment Use
volk_mallocif doing heavy SIMD work. - Exceptions Don’t throw from
work().
When not to convert
Keep Python if:
- Control-plane logic only
- Message-based blocks
- Low data rates
- Rapid experimentation
Convert to C++ when:
- Heavy DSP
- Correlation / FFT / accumulation
- Real-time interferometry (very relevant to your RSPduo work)