2 Prerequisites & Setup
Quick‑start: one command, zero surprises
docker pull --platform linux/amd64 ghcr.io/fair-acc/gr4-build-container:latest
🛠 Toolchain Basics (Bare‑Metal)
Tool |
Min Version |
Why |
GCC |
≥13.3 (≥14.2 for full <experimental/simd> ) |
minimum to compile GR4 |
Clang |
18 (recommended) |
faster builds, great diagnostics |
CMake |
3.25 |
presets & fetched content |
Ninja |
any |
parallel build engine |
Python |
3.10 |
unit‑test harness & bindings |
Install on Ubuntu 24.04:
sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y
sudo apt update
sudo apt install gcc-14 g++-14 clang-18 cmake ninja-build git python3-pip -y
export CC=gcc-14 CXX=g++-14 # or clang-18/clang++-18
🐳 Docker Route (Recommended for Beginners)
- Pull the container (see quick‑start).
- Run with the current repo mounted:
docker run --rm -it \
--volume="${PWD}:/work/src" \
--workdir="/work/src" \
ghcr.io/fair-acc/gr4-build-container:latest bash
3. Inside the shell, configure + build:
rm -rf build && mkdir build && cd build
cmake -DCMAKE_BUILD_TYPE=RelWithAssert -DGR_ENABLE_BLOCK_REGISTRY=ON ..
cmake --build . -j$(nproc)
ctest --output-on-failure -j$(nproc)
💡 ZRAM Route (If Your Laptop Runs Out of RAM)
Building GR4 can transiently spike to 6 GB+ of RAM with GCC's heavy templates. If your system swaps to disk, compile times crawl. zram swaps to compressed RAM instead of SSD.
# Enable 8 GiB zram swap (needs sudo)
cd gnuradio4
sudo ./enableZRAM.sh # provided in repo root
# Build as usual
mkdir -p build && cd build
cmake -DCMAKE_BUILD_TYPE=RelWithAssert ..
cmake --build . -j$(nproc)
# Afterwards, free the device
sudo swapoff /dev/zram0
echo 1 | sudo tee /sys/block/zram0/reset
♻️ Rule of thumb: if free -h
shows >80 % memory used during compile, enable zram.
🗂 Working Inside the Main Repo
# 1. Fork the repo on GitHub
# 2. Clone your fork
git clone https://github.com/<you>/gnuradio4.git
cd gnuradio4
git remote add upstream https://github.com/fair-acc/gnuradio4.git
git checkout -b port/math/MultiplyConst
# ...hack...
# 3. Commit w/ DCO & push
git add .
git commit -s -m "feat(math): port MultiplyConst block (float & complex)"
git push origin port/math/MultiplyConst
# 4. Open PR → choose 'Create pull request'
Tip Use git push --force-with-lease
(not --force
) when you squash‑rebase.
5 Block API Migration
5.1 From work()
to processBulk()
(Beginner Friendly)
Old way (pseudo‑code):
std::int32_t work(std::int32_t 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 (std::int32_t i = 0; i < noutput_items; i++)
out[i] = in[i] * k_;
return noutput_items;
}
Problems: raw casts, no bounds check.
New way:
work_return_t processBulk(std::span<const float> in,
std::span<float> out) {
for (std::size_t i = 0; i < in.size(); ++i)
out[i] = in[i] * value; // 'value' is reflected param
return {out.size(), Status::OK};
}
Key points for beginners:
std::span
is like a safe pointer + length.
- The
work_return_t
tells the scheduler how many items you produced.
- No virtual calls—
processBulk()
is a normal function.
5.2 work_return_t Status Values
Unlike GR 3.x where negative return values indicated errors, GR4 uses explicit status enums for clearer semantics:
Status |
When to Use |
Example Scenario |
Status::OK |
Normal processing completed |
Successfully processed all input samples |
Status::WAIT |
Need more input data |
FIR filter doesn't have enough taps |
Status::DONE |
Block finished its work |
File source reached EOF |
Status::ERROR |
Unrecoverable error |
Invalid configuration or I/O failure |
Status::DRAIN |
Flushing remaining data |
Filter outputting final samples |
Practical Examples:
// FIR filter waiting for more input
work_return_t processBulk(std::span<const T> input, std::span<T> output) {
if (input.size() < _filter.num_taps()) {
return {0, Status::WAIT}; // Need more samples
}
// ... process normally ...
return {output.size(), Status::OK};
}
// Unrecoverable error case
work_return_t processBulk(std::span<const T> input, std::span<T> output) {
if (!_is_configured) {
return {0, Status::ERROR}; // Cannot proceed
}
// ... normal processing ...
}
⚠️ Exception Guidelines: Exceptions inside processBulk()
are discouraged—they prevent compiler optimizations and complicate the scheduler. Use explicit Status::ERROR
returns instead.
5.3 processOne vs processBulk: A Practical Example
Here's a real-world decimating filter showing when to use each function type:
template<typename T, typename TParent>
class DecimatingFilter : public gr::Block<DecimatingFilter<T, TParent>> {
FIR<T> _filter;
std::uint32_t decimate = 1;
public:
// Use processOne for constant-rate processing (1:1 ratio)
[[nodiscard]] T processOne(T input) noexcept
requires(TParent::ResamplingControl::kIsConst)
{
return _filter.processOne(input);
}
// Use processBulk for variable-rate processing (N:M ratio)
[[nodiscard]] work::Status processBulk(std::span<const T> input, std::span<T> output) noexcept
requires(not TParent::ResamplingControl::kIsConst)
{
assert(output.size() >= input.size() / decimate); // optional, usually not needed
std::size_t out_sample_idx = 0;
for (std::size_t i = 0; i < input.size(); ++i) {
T output_sample = _filter.processOne(input[i]);
if (i % decimate == 0) {
output[out_sample_idx++] = output_sample;
}
}
return work::Status::OK;
}
};
Key Design Points:
processOne
for simple 1:1 transformations (compile-time guaranteed)
processBulk
for complex N:M ratios, state management, or conditional logic
- Use
std::uint32_t
, std::size_t
instead of plain int
for clarity
- Concepts (
requires
) ensure only the appropriate function is called
5.4 Declaring Ports
GR_DECLARE_PORT(in, PortIn<float>);
GR_DECLARE_PORT(out, PortOut<float>);
That's it—no magic numbers like input_items[0]
.
5.5 Reflection One‑Liner
GR_MAKE_REFLECTABLE(MultiplyConst,
(float, value, "Multiplier", 1.0f, "Constant gain factor"));
This auto‑generates (CMake script picks up the reflection data and writes YAML + Python stubs):
- YAML entry for future GUI.
- Python binding (
gr.blocks.math.multiply_const
).
- Run‑time property that can be tweaked live.
6 SIMD Optimisation
std::simd
is headed for the post-C++23 standard; compilers already ship it under <experimental/simd>
. It gives you auto‑vectorisation without writing AVX/NEON intrinsics. GR4's meta‑helpers make it trivial to support both scalar and vector paths.
6.1 Detecting Whether SIMD Is Available
#include <experimental/simd>
#if defined(__cpp_lib_simd) // GCC ≥13 / Clang ≥16
// then <experimental/simd> is available
#endif
GR4 wraps this behind the convenience concept gr::meta::any_simd<V,T>
used in the generated processOne()
template.
6.2 A Minimal Example
Below is the SIMD‑aware branch extracted from Analog.hpp (MultiplyConst):
template<gr::meta::t_or_simd<T> V>
[[nodiscard]] constexpr V processOne(const V& a) const noexcept {
if constexpr (gr::meta::any_simd<V, T>) {
return a * value; // element‑wise vector multiply (AVX/NEON)
} else {
return a * value; // plain scalar – same line, different type
}
}
Note the single line of math appears twice—one inside the SIMD branch, one outside. Most real blocks need no extra code: the compiler emits the vector loop for you.
6.3 Fallback Strategy
If your compiler is older than GCC 13/Clang 16, __cpp_lib_simd
is undefined and the scalar branch is chosen. Performance degrades gracefully, functionality remains identical.
6.4 Benchmarking Tips
Tip |
Command |
Build with ‑O3 ‑march=native |
cmake -DCMAKE_CXX_FLAGS="-O3 -march=native" .. |
Time one block |
gr_benchmark -b math::MultiplyConst -n 10M |
Check assembly |
objdump -dS libgnuradio‑math.so | grep -E "(ymm|zmm|vq)" |
Rule of thumb: if you see vfmadd
or vmulps
in the disassembly, SIMD is working.
7 Reflection & Registration System
7.1 Why Reflection?
GR4 can introspect a block at run‑time—ports, parameters, doc‑strings—thanks to a tiny header‑only reflection system. This powers future GUI builders and Python auto‑bindings.
7.2 Dissecting an Example
struct MultiplyConst : gr::Block<MultiplyConst> {
PortIn<float> in;
PortOut<float> out;
float value = 1.0f;
GR_MAKE_REFLECTABLE(MultiplyConst, in, out, value);
};
GR_MAKE_REFLECTABLE
:
- Registers each public member (
in
, out
, value
).
- Emits metadata (type, default, doc string).
- Auto‑generates setters/getters used by Python and YAML.
7.3 Registration One‑Liner
GR_REGISTER_BLOCK("gr::blocks::math::MultiplyConst",
MultiplyConst,
([T], std::multiplies<[T]>), // template params & functor
[ float, double, std::complex<float>, std::complex<double> ])
Parameters:
- Fully‑qualified name – becomes YAML path and Python import path.
- C++ symbol – the class or alias to instantiate.
- Template pack – how to splice the functor/type into the template.
- Type list – every concrete data type you want exposed.
7.4 Hot‑Reloading Parameters
Because value
is reflected, you can change it in a flowgraph at run‑time:
blk = gr.blocks.math.multiply_const_f() # Python auto‑binding
blk.value = 0.5 # halves the gain while the graph runs
No extra C++ needed!
8 Testing & Validation
A good port is bit‑exact and has unit tests covering corner cases. GR4 ships a thin Boost.UT harness in gnuradio‑4.0/testing
.
8.1 Minimal Test Skeleton
#include <boost/ut.hpp>
#include <gnuradio-4.0/testing/TagMonitors.hpp>
using namespace boost::ut;
using gr::blocks::math::MultiplyConst;
"MultiplyConst scalar"_test = [] {
constexpr float k = 2.0f;
MultiplyConst<float> blk({{"value", k}});
expect(eq(blk.processOne(3.0f), 6.0f));
};
8.2 Graph‑Level Tests (qa_analog.cpp)
qa_analog.cpp
wires sources → DUT → sink inside an in‑memory graph and validates the full scheduler path.
Graph g;
auto& src = g.emplaceBlock<TagSource<float>>(
property_map{{"values", {1,2,3}}}
);
auto& mul = g.emplaceBlock<MultiplyConst<float>>(
property_map{{"value",2.0f}}
);
auto& snk = g.emplaceBlock<TagSink<float, ProcessFunction::USE_PROCESS_ONE>>();
g.connect(src,"out",mul,"in");
g.connect<"out">(mul).to<"in">(snk);
expect(eq(scheduler::Simple{std::move(g)}.runAndWait().has_value(), true));
8.3 CI Tips
Add a quick workflow to .github/workflows/ci.yml
:
- name: Build & test (Clang 18)
run: |
cmake -B build -S . -DCMAKE_CXX_COMPILER=clang++-18 -GNinja
ninja -C build -j$(nproc)
ctest --test-dir build -j$(nproc) --output-on-failure
Enable multiple jobs for GCC/Clang, and add ASAN if possible.
9 Complete Porting Examples
This section walks through three complete ports from scratch, showing the full transformation from GR3 to GR4 code.
9.1 Simple Math Block: MultiplyConst
GR3 Original:
// From gr-blocks/lib/multiply_const_ff_impl.cc
class multiply_const_ff_impl : public multiply_const_ff {
float d_k;
public:
multiply_const_ff_impl(float k) : d_k(k) {}
void set_k(float k) { d_k = k; }
float k() const { return d_k; }
int work(int noutput_items,
gr_vector_const_void_star& input_items,
gr_vector_void_star& output_items) override {
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_k;
}
return noutput_items;
}
};
GR4 Port:
// From gr4/blocks/math/Analog.hpp
template<typename T>
struct MultiplyConst : public gr::Block<MultiplyConst<T>> {
PortIn<T> in;
PortOut<T> out;
T value = T{1}; // reflected parameter
// SIMD-aware processing
template<gr::meta::t_or_simd<T> V>
[[nodiscard]] constexpr V processOne(const V& a) const noexcept {
if constexpr (gr::meta::any_simd<V, T>) {
return a * value; // vectorized multiply
} else {
return a * value; // scalar multiply
}
}
GR_MAKE_REFLECTABLE(MultiplyConst, in, out, value);
};
// Registration for all numeric types
GR_REGISTER_BLOCK("gr::blocks::math::MultiplyConst", MultiplyConst,
[T], std::multiplies<T>,
[float, double, std::complex<float>, std::complex<double>]);
Key Changes:
- ✅ 10× less code – single template instead of separate _ff, _cc variants
- ✅ Type safety –
PortIn<T>
vs raw void pointers
- ✅ SIMD support – automatic vectorization when available
- ✅ Reflection – parameter is automatically exposed to Python/YAML
- ✅ Zero boilerplate – no getter/setter methods needed
9.2 Stateful Block: Integrate
GR3 Original:
// From gr-blocks/lib/integrate_ff_impl.cc
class integrate_ff_impl : public integrate_ff {
std::uint32_t d_decim;
std::uint32_t d_count;
float d_sum;
public:
integrate_ff_impl(std::uint32_t decim) : d_decim(decim), d_count(0), d_sum(0) {}
std::int32_t work(std::int32_t noutput_items,
gr_vector_const_void_star& input_items,
gr_vector_void_star& output_items) override {
const float* in = (const float*)input_items[0];
float* out = (float*)output_items[0];
std::int32_t j = 0;
for (std::int32_t i = 0; i < noutput_items * d_decim; i++) {
d_sum += in[i];
d_count++;
if (d_count == d_decim) {
out[j++] = d_sum;
d_sum = 0;
d_count = 0;
}
}
return j;
}
};
GR4 Port:
// From gr4/blocks/math/Analog.hpp
template<typename T>
struct Integrate : public gr::Block<Integrate<T>> {
PortIn<T> in;
PortOut<T> out;
std::uint32_t decim = 1;
private:
std::uint32_t d_count = 0;
T d_sum = T{0};
public:
work_return_t processBulk(std::span<const T> input,
std::span<T> output) {
std::size_t j = 0;
for (std::size_t i = 0; i < input.size() && j < output.size(); ++i) {
d_sum += input[i];
d_count++;
if (d_count == decim) {
output[j++] = d_sum;
d_sum = T{0};
d_count = 0;
}
}
return {j, Status::OK};
}
GR_MAKE_REFLECTABLE(Integrate, in, out, decim);
};
Key Changes:
- ✅ Safe spans –
std::span
prevents buffer overruns
- ✅ Clear return semantics –
work_return_t
explicitly shows items produced
- ✅ Modern C++ –
std::size_t
for indices, uniform initialization
- ✅ Encapsulation – state variables are private, only
decim
is reflected
9.3 Advanced Processing: Argmax
GR3 Original:
// From gr-blocks/lib/argmax_fs_impl.cc
class argmax_fs_impl : public argmax_fs {
std::size_t d_vlen;
public:
argmax_fs_impl(std::size_t vlen) : d_vlen(vlen) {}
std::int32_t work(std::int32_t noutput_items,
gr_vector_const_void_star& input_items,
gr_vector_void_star& output_items) override {
const float* in = (const float*)input_items[0];
std::int16_t* out = (std::int16_t*)output_items[0];
for (std::int32_t i = 0; i < noutput_items; i++) {
float max_val = in[i * d_vlen];
std::size_t max_idx = 0;
for (std::size_t j = 1; j < d_vlen; j++) {
if (in[i * d_vlen + j] > max_val) {
max_val = in[i * d_vlen + j];
max_idx = j;
}
}
out[i] = static_cast<std::int16_t>(max_idx);
}
return noutput_items;
}
};
GR4 Port:
// From gr4/blocks/math/Analog.hpp
template<typename T>
struct Argmax : public gr::Block<Argmax<T>> {
PortIn<T> in;
PortOut<gr::Size_t> out;
std::size_t vlen = 1;
work_return_t processBulk(std::span<const T> input,
std::span<gr::Size_t> output) {
const std::size_t n_vectors = input.size() / vlen;
const std::size_t n_output = std::min(n_vectors, output.size());
for (std::size_t i = 0; i < n_output; ++i) {
const auto vector_start = input.subspan(i * vlen, vlen);
const auto max_it = std::max_element(vector_start.begin(),
vector_start.end());
output[i] = static_cast<gr::Size_t>(std::distance(vector_start.begin(), max_it));
}
return {n_output, Status::OK};
}
GR_MAKE_REFLECTABLE(Argmax, in, out, vlen);
};
Key Changes:
- ✅ STL algorithms –
std::max_element
is optimized and readable
- ✅ Subspan safety –
input.subspan()
prevents out-of-bounds access
- ✅ Proper typing –
gr::Size_t
output (was short
)
- ✅ Bounds checking –
std::min()
prevents buffer overrun
9.4 Common Porting Patterns
Pattern |
GR3 Anti-Pattern |
GR4 Best Practice |
Type variants |
Separate _ff, _cc, _ii files |
Single template with type registration |
Parameters |
Private + getter/setter |
Public + reflection |
Buffers |
Raw pointers + manual indexing |
std::span + bounds checking |
State |
Mix public/private inconsistently |
Private state, reflected parameters |
Algorithms |
Hand-rolled loops |
STL algorithms when possible |
10 Best Practices & Conventions
This section covers coding standards, naming conventions, and architectural patterns that make GR4 blocks maintainable and performant.
10.1 Naming Conventions
Element |
Convention |
Example |
Rationale |
Block names |
PascalCase |
MultiplyConst , FftFilter |
Matches C++ class naming |
Port names |
lowerCamel, descriptive |
in , out , taps |
Clear intent in Python |
Parameters |
snake_case |
sample_rate , cutoff_freq |
Consistent with GNU Radio tradition |
Private members |
d_ prefix |
d_history , d_taps |
Distinguishes from parameters |
Template params |
Single uppercase letter |
T , U , V |
Standard C++ convention |
10.2 Performance Best Practices
Golden Rule: Write readable code first, then optimize the hot paths.
Memory Management
- ✅ Prefer stack allocation – avoid
new
/delete
in inner loops
- ✅ Use
std::span
– zero-copy views instead of copying data
- ✅ Reserve vectors –
taps.reserve(expected_size)
prevents reallocations
- ❌ Avoid
std::vector<bool>
– use std::vector<char>
for better performance
Algorithm Optimization
- ✅ Use STL algorithms –
std::transform
, std::accumulate
are optimized
- ✅ Enable SIMD – implement
processOne()
with gr::meta::t_or_simd
- ✅ Cache-friendly access – process data sequentially when possible
- ❌ Avoid branching in inner loops – use
std::conditional
or templates
Compiler Hints
// Good: Help the compiler optimize
[[nodiscard]] constexpr auto processOne(const T& input) const noexcept {
// constexpr allows compile-time evaluation
// noexcept enables aggressive optimization
return input * gain;
}
// Bad: Runtime overhead
auto processOne(const T& input) {
if (enable_processing) { // branch in hot path
return input * gain;
}
return input;
}
10.3 Code Organization Patterns
Single Responsibility Principle
Each block should do one thing well:
// Good: Clear, focused responsibility
struct LowPassFilter : public gr::Block<LowPassFilter<T>> {
PortIn<T> in;
PortOut<T> out;
float cutoff_freq = 1000.0f;
// ... filter implementation
};
// Bad: Mixed responsibilities
struct AudioProcessor : public gr::Block<AudioProcessor<T>> {
// Does filtering, AGC, and compression - too much!
};
Template Parameter Guidelines
- ✅ Use concepts –
gr::meta::arithmetic<T>
constrains valid types
- ✅ Default to
float
– most common use case
- ✅ Support complex types –
std::complex<T>
when mathematically valid
- ❌ Don't over-template – avoid templates for configuration that could be runtime parameters
10.4 Error Handling
Input Validation
// Good: Validate in constructor or setter
struct Decimator : public gr::Block<Decimator<T>> {
int decim = 1;
void validateParameters() {
if (decim <= 0) {
throw std::invalid_argument("Decimation must be positive");
}
}
// Called automatically by reflection system
void setDecimation(int dec) {
decim = dec;
validateParameters();
}
};
Graceful Degradation
// Good: Fallback when SIMD unavailable
template<gr::meta::t_or_simd<T> V>
[[nodiscard]] constexpr V processOne(const V& input) const noexcept {
if constexpr (gr::meta::any_simd<V, T>) {
return performSIMDOperation(input);
} else {
return performScalarOperation(input); // Always works
}
}
10.5 Testing Strategy
Unit Test Coverage
- ✅ Test edge cases – zero, negative, NaN, infinity inputs
- ✅ Test all type variants – float, double, complex<float>, complex<double>
- ✅ Test parameter validation – invalid inputs should throw
- ✅ Test SIMD and scalar paths – ensure bit-exact results
Integration Tests
// Good: Test in realistic flowgraph
"MultiplyConst in flowgraph"_test = [] {
Graph g;
auto& src = g.emplaceBlock<TagSource<float>>();
auto& mul = g.emplaceBlock<MultiplyConst<float>>({{"value", 2.0f}});
auto& snk = g.emplaceBlock<TagSink<float>>();
g.connect(src, "out", mul, "in");
g.connect(mul, "out", snk, "in");
scheduler::Simple{std::move(g)}.runAndWait();
// Verify results...
};
10.6 Documentation Standards
Block Documentation
/// @brief Multiply input signal by a constant factor
///
/// This block multiplies each input sample by a constant value.
/// Supports SIMD acceleration when available.
///
/// @tparam T Input/output sample type (float, double, complex<float>, etc.)
template<typename T>
struct MultiplyConst : public gr::Block<MultiplyConst<T>> {
PortIn<T> in; ///< Input signal
PortOut<T> out; ///< Output signal (input * value)
T value = T{1}; ///< @brief Multiplication factor @unit linear
GR_MAKE_REFLECTABLE(MultiplyConst, in, out, value);
};
Parameter Documentation
- ✅ Use @brief – concise description
- ✅ Include @unit – physical units (Hz, dB, linear, etc.)
- ✅ Document constraints – valid ranges, dependencies
- ✅ Example usage – Python snippet in docstring
10.7 Common Anti-Patterns to Avoid
❌ Anti-Pattern |
✅ Better Approach |
Why |
Global state / singletons |
Dependency injection via constructor |
Testability, thread safety |
String-based configuration |
Strongly typed parameters |
Compile-time validation |
Deep inheritance hierarchies |
Composition over inheritance |
Flexibility, maintainability |
Premature optimization |
Profile-guided optimization |
Readable code first |
Magic numbers in code |
Named constants or parameters |
Self-documenting code |
11 Troubleshooting Common Issues
This section covers the most common problems encountered during porting, with step-by-step solutions.
11.1 Compilation Errors
❌ "No matching function for call to 'gr::Block'"
error: no matching function for call to 'gr::Block<MyBlock>::Block()'
note: candidate expects 1 argument, 0 provided
✅ Solution: GR4 blocks need a property map constructor:
// Bad: Missing constructor
struct MyBlock : public gr::Block<MyBlock> {
// ...
};
// Good: Add property map constructor
struct MyBlock : public gr::Block<MyBlock> {
explicit MyBlock(const property_map& params = {}) {
// Initialize from params if needed
}
// ...
};
❌ "Cannot convert 'const float*' to 'std::span<float>'"
error: cannot convert 'const float*' to 'std::span<float>' in assignment
✅ Solution: Use std::span<const T>
for input, std::span<T>
for output:
// Bad: Wrong const-ness
work_return_t processBulk(std::span<float> input, std::span<float> output);
// Good: Input should be const
work_return_t processBulk(std::span<const float> input, std::span<float> output);
❌ "GR_MAKE_REFLECTABLE not found"
error: 'GR_MAKE_REFLECTABLE' was not declared in this scope
✅ Solution: Include the reflection header:
#include <gnuradio-4.0/Block.hpp>
#include <gnuradio-4.0/reflection.hpp> // Add this line
11.2 Runtime Errors
❌ "Block not found in registry"
RuntimeError: Block 'gr::blocks::math::MyBlock' not found in registry
✅ Solution: Ensure your block is registered and linked:
// 1. Add registration at end of header
GR_REGISTER_BLOCK("gr::blocks::math::MyBlock", MyBlock, [T], [float, double]);
// 2. Make sure it's compiled into the library
// Check CMakeLists.txt includes your header
// 3. Verify it's linked
#include <gnuradio-4.0/math/MyBlock.hpp> // Force instantiation
❌ "Scheduler hangs or crashes"
Program hangs indefinitely or crashes with segmentation fault
✅ Solution: Check your work return and buffer handling:
// Bad: Wrong return value
work_return_t processBulk(std::span<const T> input, std::span<T> output) {
// ... process data ...
return {input.size(), Status::OK}; // WRONG: should return output.size()
}
// Good: Return actual items produced
work_return_t processBulk(std::span<const T> input, std::span<T> output) {
std::size_t items_produced = std::min(input.size(), output.size());
// ... process data ...
return {items_produced, Status::OK};
}
❌ "Port connection failed"
RuntimeError: Cannot connect incompatible port types
✅ Solution: Check port type compatibility:
// Bad: Type mismatch
PortIn<float> in;
PortOut<double> out; // Different types!
// Good: Consistent typing
PortIn<T> in;
PortOut<T> out;
11.3 Performance Issues
❌ "Block is slower than GR3 version"
✅ Debugging checklist:
- Check compiler flags:
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_FLAGS="-O3 -march=native" ..
- Verify SIMD is working:
objdump -dS libgnuradio-math.so | grep -E "(ymm|zmm|vfmadd)"
- Profile hot paths:
perf record -g ./your_flowgraph
perf report
- Check memory allocation:
valgrind --tool=massif ./your_flowgraph
❌ "High CPU usage in scheduler"
✅ Solution: Optimize your processOne/processBulk implementation:
// Bad: Inefficient inner loop
for (std::size_t i = 0; i < input.size(); ++i) {
output[i] = std::sin(input[i]); // Expensive call per sample
}
// Good: Vectorized operation
std::transform(input.begin(), input.end(), output.begin(),
[](const auto& x) { return std::sin(x); });
11.4 SIMD-Related Issues
❌ "SIMD code compiles but crashes"
Segmentation fault in SIMD code path
✅ Solution: Check alignment and bounds:
// Bad: Assumes aligned data
template<gr::meta::t_or_simd<T> V>
V processOne(const V& input) {
// May crash if input not aligned
return input * 2.0f;
}
// Good: Let compiler handle alignment
template<gr::meta::t_or_simd<T> V>
V processOne(const V& input) {
if constexpr (gr::meta::any_simd<V, T>) {
return input * static_cast<V>(2.0f); // Explicit cast
} else {
return input * 2.0f;
}
}
❌ "SIMD not being used despite compiler support"
# No SIMD instructions in assembly output
✅ Solution: Check your template constraints:
// Bad: Missing SIMD template
void processOne(const T& input) {
// Only scalar version
}
// Good: SIMD-aware template
template<gr::meta::t_or_simd<T> V>
[[nodiscard]] constexpr V processOne(const V& input) const noexcept {
if constexpr (gr::meta::any_simd<V, T>) {
return input * value; // SIMD path
} else {
return input * value; // Scalar path
}
}
11.5 Testing and Validation Issues
❌ "Unit tests fail intermittently"
✅ Solution: Check for floating-point precision issues:
// Bad: Exact comparison
expect(eq(result, expected));
// Good: Tolerance-based comparison
expect(approx(result, expected, 1e-6f));
❌ "Tests pass but flowgraph produces wrong output"
✅ Solution: Add integration tests with realistic data:
// Test with actual signal processing chain
"Integration test with realistic signal"_test = [] {
Graph g;
auto& src = g.emplaceBlock<SignalSource<float>>({
{"frequency", 1000.0f},
{"sample_rate", 48000.0f}
});
auto& dut = g.emplaceBlock<MyBlock<float>>();
auto& snk = g.emplaceBlock<VectorSink<float>>();
// ... connect and run ...
// Verify output characteristics
auto output = snk.getData();
expect(output.size() > 0);
// Check frequency domain, RMS, etc.
};
11.6 Build System Issues
❌ "CMake can't find GR4 components"
CMake Error: Could not find a configuration file for package "gnuradio"
✅ Solution: Set the correct CMake paths:
# Option 1: Set CMAKE_PREFIX_PATH
export CMAKE_PREFIX_PATH=/usr/local/lib/cmake/gnuradio:$CMAKE_PREFIX_PATH
# Option 2: Use find_package with PATHS
find_package(gnuradio REQUIRED PATHS /usr/local/lib/cmake/gnuradio)
# Option 3: Install GR4 to standard location
cmake --install build --prefix /usr/local
❌ "Linking errors with undefined symbols"
undefined reference to `gr::blocks::math::MyBlock<float>::MyBlock()'
✅ Solution: Ensure proper template instantiation:
// In your .cpp file (if you have one)
template class MyBlock<float>;
template class MyBlock<double>;
template class MyBlock<std::complex<float>>;
template class MyBlock<std::complex<double>>;
// Or use explicit instantiation in header
extern template class MyBlock<float>;
11.7 Debugging Techniques
Enable Debug Logging
# Set environment variable
export GR_LOG_LEVEL=DEBUG
# Or in code
gr::log::set_level(gr::log::Level::DEBUG);
Use GDB for Crashes
# Compile with debug symbols
cmake -DCMAKE_BUILD_TYPE=Debug ..
# Run with GDB
gdb ./your_program
(gdb) run
(gdb) bt # when it crashes
AddressSanitizer for Memory Issues
# Enable ASAN
cmake -DCMAKE_CXX_FLAGS="-fsanitize=address -g" ..
# Run your program - it will catch memory errors
11.8 Common Gotchas
❌ Common Mistake |
✅ Correct Approach |
Why It Matters |
Using std::vector in processOne |
Use fixed-size arrays or class members |
Allocation in hot path kills performance |
Forgetting const on input spans |
std::span<const T> for inputs |
Prevents accidental modification |
Returning wrong item count |
Return actual items produced |
Scheduler needs accurate counts |
Missing noexcept on processOne |
Mark processOne as noexcept |
Enables compiler optimizations |
Not testing edge cases |
Test with zero, NaN, infinity |
Real-world data is messy |