This folder holds the in-tree blocks, which depend on the runtime library. Each module of blocks is within its own folder.
The create_block.py
script is a stripped down modtool surrogate that drops a block directory into the desired location
cd blocklib/[MODULE]
python3 utils/modtool/create_block.py --intree [BLOCKNAME] --author "[AUTHOR NAME]"
If this block is going to be templated (such as the add or vector_source blocks), then add the --templated
flag
This creates a directory with the following files:
.
├── .gitignore
├── [BLOCKNAME]_cpu.cc
├── [BLOCKNAME]_cpu.h
└── [BLOCKNAME].yml
The .yml file is the landing point for all the interfaces to the block
Edit it according to the desired parameters, ports, callbacks, etc.
More detail to come about the yml spec
Open [BLOCKNAME]_cpu.h
which maps roughly to the _impl.h
file from GR3
Add any private variables that will
The make function is no longer needed as this is automatically generated
The constructor code looks like GR3 except
- No io_signature - this is handled in the auto-generated code from the yml
- No msg ports - also handled from the auto-generated code
The constructor has the form:
[BLOCKNAME]_cpu::[BLOCKNAME]_cpu(block_args args) : INHERITED_CONSTRUCTORS {}
The INHERITED_CONSTRUCTORS macro covers the initialization of the base classes since the block class is virtual, this would require initializing the same thing multiple times. In the case of a templated block class, INHERITED_CONSTRUCTORS takes the template parameter, e.g. INHERITED_CONSTRUCTORS(T).
Each of the parameters that are defined in the .yml
are available as a member of the args
struct. So what previously would have been a parameter to the constructor is now lumped into this struct so the constructor signature does not change.
Work functions are conceptually the same as GR3, but syntactically slightly different to keep the block API separated from the scheduler API.
work_return_code_t work(work_io&)
override;
First, the work function takes in a vector of pointers to input objects, and a vector of pointers to output objects.
To get the noutput_items
would be
auto noutput_items = work_output[0].n_items;
block_work_input_sptr
contains a pointer to the underlying buffer object - it is not just raw pointers. But it is easy to get the underlying pointer
auto in = wio.inputs()[0].items<T>();
auto out = wio.outputs()[0].items<T>();
Where T
is whatever specific type for a non-templated block
The return value of the work function is a return code instead of the number of output items produce. Therefore, produce
and consume
must be called from within the work function. This keeps some consistency between sync and general blocks.
wio.produce_each(noutput_items);
or
wio.outputs()[0].n_produced = noutput_items;
and then
return work_return_code_t::WORK_OK;
When a message port is added in the yml, an automatically named handler is instatiated at the top level block class (autogenerated)
For example, for an input message port with id: in
ports:
- domain: message
id: in
direction: input
optional: true
a virtual method is expected to be implemented in the underlying block class named
void handle_msg_in(pmtf::pmt msg) override
Publicly accessible methods (not necessary for parameter handlers, e.g. k() and set_k()) - but generic methods must be defined in the yaml, and then implemented in the _cpu.cc
More definition is needed into how these fields map, but the basic idea is this
callbacks:
- id: last_endpoint
return: std::string
const: true
gets autogenerated into the blockname.h
as
virtual std::string last_endpoint() const = 0;
then must be implemented
There is some big TODO
s here to make this more robust and prevent creating a new mini-language to generate some basic c++