Chapter 9: Fields - Get() and Set()#
In chapter 7 we introduced fields – events “on steroids” that always carry a
current value. There, however, we only used the notifier part of a field (tag WithNotifier): the consumer
subscribed to the field and received value updates event-driven via GetNewSamples().
A field can, in addition to (or instead of) its notifier, offer a request/response style getter (Get()) and
setter (Set()). These are enabled via the WithGetter / WithSetter tags. We deferred them to this chapter
because – semantically – Get() and Set() on a field are just methods, and methods were only introduced in
chapter 8.
This chapter takes the TirePressureService of chapter 7 as its basis, renames it to TirePressureExtendedService
(tire_pressure_extended_service.h / .cpp) and extends it to demonstrate Get() and Set().
Warning
Current limitation (please read first): The provider-side handler that serves a field getter is not yet
auto-registered by the middleware (see the TODO: Remove this skip once the field Get handler is auto-registered in
SkeletonField. in score/mw/com/impl/bindings/lola/skeleton.cpp). As a consequence, while the code of this
chapter builds and links cleanly, a provider will currently abort as soon as a consumer subscribes to a field
Get(). Set() (i.e. WithSetter) already works end-to-end; the WithGetter/Get() path does not yet.
This chapter is therefore written to be complete and correct against the public API, so that it runs as soon as the
middleware gap is closed. The “How to run the example” section below reflects this.
Get() vs. the notifier - why does a getter exist at all?#
At first glance a getter looks redundant: with WithNotifier a consumer already receives every field value and can
read the latest one via GetNewSamples(). So why would one want Get()?
The key is to understand what the notifier actually requires: whenever the provider updates the field value, that
update has to be made visible to a subscribed consumer, i.e. the latest value has to end up in some buffer local
to the subscriber, because that is exactly where GetNewSamples() looks. What “local to the subscriber” means,
however, is binding-specific:
Setup 1 – network binding (separate ECUs): On every field update the provider has to send the new value over the network to the subscriber node, where it is stored in a local buffer that
GetNewSamples()then reads.Setup 2 – IPC binding with per-process buffers (same ECU, different processes): On every field update the provider has to copy the new value into the buffer of the subscriber’s process/address space, from where
GetNewSamples()reads it.Setup 3 – IPC binding with shared-memory buffers (same ECU, different processes): Here provider and subscriber share the very same buffer for the field samples. There is nothing extra for the provider to do after updating the value – enabling
WithNotifieris essentially a no-op on the provider side.
Now assume the provider updates the field at a very high rate. In Setup 1 (and, to a lesser extent, Setup 2)
enabling WithNotifier then creates real cost: CPU load in both cases and additional network traffic in Setup 1. If a
consumer does not need every single update instantly – e.g. because it only uses the latest value in some rarely
scheduled activity – keeping WithNotifier active would just waste resources. Such a consumer would instead
disable the notifier, enable WithGetter and call Get() just in time, exactly when it needs the current
value.
In Setup 3, on the other hand, where WithNotifier carries no added cost, a consumer could simply keep the notifier
and always access the latest sample via GetNewSamples().
From an academic point of view it is somewhat unfortunate when a developer, who implements solely against the (binding
agnostic) public API of score::mw::com, lets binding-specific behaviour drive the design of the service
interface. Deciding to use WithNotifier and not WithGetter by assuming that the application will always run in
a Setup 3 situation is problematic: at least in theory, the person deploying the application (who may be different
from the one designing the interface) could change the configuration so that it is not a Setup 3 situation anymore.
Bottom line: there is no real semantic difference between “WithNotifier + read via GetNewSamples()” and
“WithGetter + read via Get()”. It is rather a question of efficiency, which depends on the field update rate
and the concrete binding being used.
The service interface#
The interface is based on chapter 7’s TirePressureInterface, renamed to TirePressureExtendedInterface. Compared
to chapter 7 the following changed:
The four per-wheel fields (
tire_pressure_front_left, …) are kept, but their tag changed fromWithNotifierto WithGetter.A fifth field
tire_pressure_thresholdswas added. Its data type is astruct TirePressureThresholdwith twofloatmemberslower_thresholdandupper_threshold. It is declared with WithGetter and WithSetter.An event
tire_pressure_warningwas added, whose data type is anenumidentifying a specific tire.
The event data type is a plain enum with a fixed underlying type (so it stays trivially copyable):
enum class Tire : std::uint8_t
{
front_left,
front_right,
rear_left,
rear_right,
};
The value type of the new field is a small, trivially copyable struct (a field getter/setter transports the value as a method argument/return value through shared memory, so it must be trivially copyable – exactly the same constraint we saw for method arguments in chapter 8):
struct TirePressureThreshold
{
float lower_threshold;
float upper_threshold;
};
The interface itself:
template <typename Trait>
class TirePressureExtendedInterface : public Trait::Base
{
public:
using Trait::Base::Base;
typename Trait::template Field<float, score::mw::com::WithGetter> tire_pressure_front_left{
*this,
"tire_pressure_front_left"};
typename Trait::template Field<float, score::mw::com::WithGetter> tire_pressure_front_right{
*this,
"tire_pressure_front_right"};
typename Trait::template Field<float, score::mw::com::WithGetter> tire_pressure_rear_left{
*this,
"tire_pressure_rear_left"};
typename Trait::template Field<float, score::mw::com::WithGetter> tire_pressure_rear_right{
*this,
"tire_pressure_rear_right"};
// The threshold band a consumer may both read (WithGetter) and adjust (WithSetter).
typename Trait::template Field<TirePressureThreshold, score::mw::com::WithGetter, score::mw::com::WithSetter>
tire_pressure_thresholds{*this, "tire_pressure_thresholds"};
// Notifies consumers about a tire whose current pressure has left the configured threshold band.
typename Trait::template Event<Tire> tire_pressure_warning{*this, "tire_pressure_warning"};
};
Files/artifacts used#
The bazel project for this chapter is located in score/mw/com/doc/tutorial/chapter_9. It follows the same
structure as the previous chapters, with the service interface renamed to tire_pressure_extended_service:
File Name |
Description |
|---|---|
This file contains bazel targets for this example. |
|
Implementation of the service consumer app. The main() for the consumer |
|
Header (empty - we just always want to have cpp/h pairs) of the service consumer. |
|
This file contains the configuration for score::mw::com for the consumer app. |
|
This file contains the configuration for the logging system used by score::mw::com |
|
Implementation of the service provider. |
|
Header (empty) of the service provider. |
|
This file contains the configuration for the logging system used by score::mw::com |
|
This file contains the configuration for score::mw::com for the provider app. |
|
This file is empty as the service interface is completely defined in the header. |
|
This file contains the definition of the service interface (fields + event). |
Configuration#
The service type binding lists the five fields (each with a fieldId) and the new event (with an
eventId). Note that a field getter/setter does not need its own methodId: internally the get/set methods are
derived from the fieldId of the field they belong to, so the configuration of a field is unchanged whether or not it
has a getter/setter.
"fields": [
{
"fieldName": "tire_pressure_front_left",
"fieldId": 1
},
{
"fieldName": "tire_pressure_front_right",
"fieldId": 2
},
{
"fieldName": "tire_pressure_rear_left",
"fieldId": 3
},
{
"fieldName": "tire_pressure_rear_right",
"fieldId": 4
},
{
"fieldName": "tire_pressure_thresholds",
"fieldId": 5
}
],
"events": [
{
"eventName": "tire_pressure_warning",
"eventId": 6
}
]
On the consumer side there is one important difference to a pure-notifier field (chapter 7): because
Get()/Set() are methods, and – just like the regular methods of chapter 8 – a consumer that wants to call them
has to list the corresponding fields in its own service instance. The optional useGetIfAvailable /
useSetIfAvailable flags declare that the consumer intends to use the getter/setter of the field:
"fields": [
{
"fieldName": "tire_pressure_front_left",
"useGetIfAvailable": true
},
{
"fieldName": "tire_pressure_front_right",
"useGetIfAvailable": true
},
{
"fieldName": "tire_pressure_rear_left",
"useGetIfAvailable": true
},
{
"fieldName": "tire_pressure_rear_right",
"useGetIfAvailable": true
},
{
"fieldName": "tire_pressure_thresholds",
"useGetIfAvailable": true,
"useSetIfAvailable": true
}
]
Note
The useGetIfAvailable / useSetIfAvailable flags are the field-accessor counterpart of the method use
property described in chapter 8: they are pure resource-tweaking hints, only
evaluated on the proxy/consumer side, and – just like the method use property – they default to ``true`` (the
getter/setter is enabled by default). Enabling a getter/setter makes the proxy set up the resources for it (for the
LoLa binding: the shared-memory of the corresponding get/set request/response queue); if a consumer is not going to
read or write a field via Get()/Set() at all, set the corresponding flag to false to spare that
shared-memory. So they share the exact same default polarity as a method’s use: enabled by default, opt out via
useGetIfAvailable: false / useSetIfAvailable: false.
Provider application#
The provider is based on chapter 7’s provider. The essential additions are:
1. A set-handler for tire_pressure_thresholds. Because Set() on a field is a method, its handler has to be
registered (via RegisterSetHandler()) before the service is offered. The handler receives the
consumer-requested value by (mutable) reference, validates/clamps it in place and thereby determines the value that
actually becomes the new field value (the middleware updates the field with whatever the handler leaves behind). The
handler clamps lower_threshold to [1.5, 2.5] and upper_threshold to [2.5, 3.5] bar, logging an error
whenever it has to adjust a value:
// it.
const auto set_handler_result =
service_instance.tire_pressure_thresholds.RegisterSetHandler([](TirePressureThreshold& value) noexcept {
value.lower_threshold =
ClampThreshold("lower_threshold", value.lower_threshold, kLowerThresholdMin, kLowerThresholdMax);
value.upper_threshold =
ClampThreshold("upper_threshold", value.upper_threshold, kUpperThresholdMin, kUpperThresholdMax);
{
std::lock_guard<std::mutex> lock{g_thresholds_mutex};
g_current_thresholds = value;
}
std::cout << "tire_pressure_thresholds set to [lower: " << value.lower_threshold
<< " bar, upper: " << value.upper_threshold << " bar]" << std::endl;
});
SCORE_LANGUAGE_FUTURECPP_ASSERT_PRD_MESSAGE(set_handler_result.has_value(),
Note that there is no explicit “get-handler”: the getter of a field always returns the field’s current value,
so the provider only has to keep the field value up to date via Update() (exactly as in chapter 7). As with every
field, the provider must supply an initial value for each field before offering the service:
// call fail.
std::cout << "Setting initial field values (before offering the service):" << std::endl;
UpdateField(service_instance.tire_pressure_front_left, "tire_pressure_front_left", kInitialTirePressure);
UpdateField(service_instance.tire_pressure_front_right, "tire_pressure_front_right", kInitialTirePressure);
UpdateField(service_instance.tire_pressure_rear_left, "tire_pressure_rear_left", kInitialTirePressure);
UpdateField(service_instance.tire_pressure_rear_right, "tire_pressure_rear_right", kInitialTirePressure);
const TirePressureThreshold initial_thresholds{kInitialLowerThreshold, kInitialUpperThreshold};
const auto thresholds_update_result = service_instance.tire_pressure_thresholds.Update(initial_thresholds);
SCORE_LANGUAGE_FUTURECPP_ASSERT_PRD_MESSAGE(thresholds_update_result.has_value(),
"Failed to set initial 'tire_pressure_thresholds' value!");
std::cout << " tire_pressure_thresholds = [lower: " << initial_thresholds.lower_threshold
<< " bar, upper: " << initial_thresholds.upper_threshold << " bar]" << std::endl;
auto offer_result = service_instance.OfferService();
SCORE_LANGUAGE_FUTURECPP_ASSERT_PRD_MESSAGE(offer_result.has_value(),
"Failed to offer TirePressureExtendedSkeleton instance!");
2. Cyclic updates and warnings. As in chapter 7 the provider updates the four tire pressures at a randomized
cadence, but now the random pressures are drawn from [1.2, 4.0] bar. After each update cycle it reads the current
threshold band and, for every tire whose new pressure has left the band, sends a tire_pressure_warning event
carrying the affected tire (several warnings may be sent after a single cycle):
std::cout << "Updating tire pressures:" << std::endl;
std::array<float, 4U> new_pressures{};
for (std::size_t index = 0U; index < tires.size(); ++index)
{
auto& [field, field_name, tire] = tires[index];
new_pressures[index] = UpdateField(field, field_name, pressure_distribution(random_engine));
}
// Read the current threshold band and send a "tire_pressure_warning" event for every tire whose new pressure
// has left it. Multiple warnings may be sent after a single update cycle.
TirePressureThreshold thresholds{};
{
std::lock_guard<std::mutex> lock{g_thresholds_mutex};
thresholds = g_current_thresholds;
}
for (std::size_t index = 0U; index < tires.size(); ++index)
{
const auto& [field, field_name, tire] = tires[index];
const float pressure = new_pressures[index];
if ((pressure < thresholds.lower_threshold) || (pressure > thresholds.upper_threshold))
{
std::cout << " '" << field_name << "' (" << pressure
<< " bar) is outside the threshold band [lower: " << thresholds.lower_threshold
<< " bar, upper: " << thresholds.upper_threshold << " bar]. Sending tire_pressure_warning."
<< std::endl;
const auto send_result = service_instance.tire_pressure_warning.Send(tire);
if (!send_result)
{
std::cerr << "Failed to send tire_pressure_warning for '" << field_name
<< "': " << send_result.error() << std::endl;
}
}
Consumer application#
The consumer is based on chapter 7’s consumer, but its reception model is inverted: it does not subscribe to the tire pressure fields anymore (they no longer have a notifier). Instead it:
subscribes to the
tire_pressure_warningevent (withmax_sample_count4, one per tire) and registers a receive handler for it,and, whenever a warning arrives, actively reads the values it needs via the field getters.
Registering the receive handler and subscribing looks exactly like the event reception of chapter 5 / the field notifier of chapter 7:
// EventReceiveHandler.
const auto set_receive_handler_result =
proxy_ptr->tire_pressure_warning.SetReceiveHandler([proxy_ptr]() noexcept {
HandleWarnings(*proxy_ptr);
});
if (!set_receive_handler_result)
{
std::cerr << "Failed to set receive handler for 'tire_pressure_warning': "
<< set_receive_handler_result.error() << ". Terminating." << std::endl;
std::exit(EXIT_FAILURE);
}
const auto subscribe_result = proxy_ptr->tire_pressure_warning.Subscribe(kMaxSampleCount);
if (!subscribe_result)
{
std::cerr << "Failed to subscribe to 'tire_pressure_warning': " << subscribe_result.error()
<< ". Terminating." << std::endl;
std::exit(EXIT_FAILURE);
}
The receive handler drains all new warning samples via GetNewSamples() and reacts to each of them:
void HandleWarnings(TirePressureExtendedProxy& proxy)
{
auto get_new_samples_result = proxy.tire_pressure_warning.GetNewSamples(
[&proxy](auto&& sample) {
HandleSingleWarning(proxy, *sample.Get());
},
kMaxSampleCount);
if (!get_new_samples_result)
{
std::cerr << "Failed to get new tire_pressure_warning samples: " << get_new_samples_result.error() << std::endl;
}
}
For each warning the consumer reads – via Get() – the current pressure of the affected tire and the current
threshold band, logs both, and then adjusts the threshold band: if the pressure was below the lower threshold it
lowers lower_threshold by 0.1 bar; if it was above the upper threshold it raises upper_threshold by
0.1 bar. The new band is written back via Set(), whose return value carries the value the provider actually
accepted (the provider may have clamped it):
void HandleSingleWarning(TirePressureExtendedProxy& proxy, const Tire tire)
{
// Read the current pressure of the affected tire via the field's Get() (request/response, not a subscription).
auto pressure_result = GetTireField(proxy, tire).Get();
if (!pressure_result.has_value())
{
std::cerr << "Failed to Get() pressure of '" << TireName(tire) << "': " << pressure_result.error() << std::endl;
return;
}
const float pressure = *(pressure_result.value());
// Read the current threshold band via the field's Get().
auto thresholds_result = proxy.tire_pressure_thresholds.Get();
if (!thresholds_result.has_value())
{
std::cerr << "Failed to Get() tire_pressure_thresholds: " << thresholds_result.error() << std::endl;
return;
}
const TirePressureThreshold thresholds = *(thresholds_result.value());
std::cout << "Received tire_pressure_warning for '" << TireName(tire) << "': current pressure = " << pressure
<< " bar; current thresholds = [lower: " << thresholds.lower_threshold
<< " bar, upper: " << thresholds.upper_threshold << " bar]." << std::endl;
// Widen the band in the direction the pressure left it.
TirePressureThreshold new_thresholds = thresholds;
if (pressure < thresholds.lower_threshold)
{
new_thresholds.lower_threshold -= kThresholdAdjustStep;
}
else if (pressure > thresholds.upper_threshold)
{
new_thresholds.upper_threshold += kThresholdAdjustStep;
}
else
{
// The pressure is within the band - nothing to adjust.
return;
}
auto set_result = proxy.tire_pressure_thresholds.Set(new_thresholds);
if (!set_result.has_value())
{
std::cerr << "Failed to Set() tire_pressure_thresholds: " << set_result.error() << std::endl;
return;
}
// The set operation returns the value the provider actually accepted (it may have clamped our request).
const TirePressureThreshold accepted = *(set_result.value());
std::cout << " Adjusted tire_pressure_thresholds to [lower: " << accepted.lower_threshold
<< " bar, upper: " << accepted.upper_threshold << " bar]." << std::endl;
}
Note that reading a field via Get() is completely independent of a subscription: Get() is a request/response
method call, so the consumer never subscribes to the tire pressure fields at all.
How to run the example#
Warning
As noted at the top of this chapter, the field getter path is not yet functional in the current middleware. The
targets build successfully, but running provider + consumer will currently make the provider abort as soon
as the consumer subscribes to a field Get(). The instructions below are provided for completeness and will work
unchanged once the middleware auto-registers the field get handler.
The steps are the same as in the previous chapters. The consumer uses the implicit configuration loading, so it is
started without the --service_instance_manifest argument.
# Build the provider and consumer targets
bazel build //score/mw/com/doc/tutorial/chapter_9:provider-tar
bazel build //score/mw/com/doc/tutorial/chapter_9:consumer-tar
Extract both archives (e.g. in a tmp-directory):
mkdir -p /tmp/tutorial/chapter_9
tar -xf <project dir>/bazel-bin/score/mw/com/doc/tutorial/chapter_9/provider-tar.tar -C /tmp/tutorial/chapter_9/
tar -xf <project dir>/bazel-bin/score/mw/com/doc/tutorial/chapter_9/consumer-tar.tar -C /tmp/tutorial/chapter_9/
… then start the service-provider application in the 1st terminal:
cd /tmp/tutorial/chapter_9/opt/TirePressureExtendedServer
bin/provider_app
and the service-consumer in the 2nd terminal:
cd /tmp/tutorial/chapter_9/opt/TirePressureExtendedClient
bin/consumer_app
Summary#
A short summary of what we have learned in this chapter:
A field can, besides (or instead of) its notifier, offer a request/response style getter (
Get(), tagWithGetter) and setter (Set(), tagWithSetter). Semantically,Get()/Set()on a field are just methods – which is why they are covered only after chapter 8.There is no semantic difference between “
WithNotifier+GetNewSamples()” and “WithGetter+Get()”; the choice is a matter of efficiency, which depends on the field update rate and the concrete binding (network / IPC with per-process buffers / IPC with shared memory). A high-rate field that a consumer only samples occasionally is a typical case for preferring the getter over the notifier.On the provider side, a setter is served by a handler registered via
RegisterSetHandler()beforeOfferService(); the handler validates/adjusts the requested value in place and the middleware then adopts it as the new field value. A getter needs no handler: it always returns the field’s current value, which the provider keeps up to date viaUpdate()(and must initialize before offering, as for any field).On the consumer side,
field.Get()andfield.Set(value)return ascore::Resultwrapping aMethodReturnTypePtr, dereferenced to access the value. Reading viaGet()is independent of any subscription.A field getter/setter is not configured as a separate method: its method id is derived from the field’s
fieldId. However – just like a regular method (chapter 8) – a consumer that wants to callGet()/Set()must list the corresponding fields in its own service instance configuration (optionally withuseGetIfAvailable/useSetIfAvailable).Current limitation: the middleware does not yet auto-register the provider-side handler that serves a field getter, so the
WithGetter/Get()path is not runnable end-to-end at the time of writing, even though it compiles and links.