Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds a context pointer to subscriptions #107

Merged
merged 23 commits into from
Jul 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,5 @@ eProsima
Julián Bermúdez Ortega <[email protected]>

Loïc Dauphin <[email protected]>

Brett Downing <[email protected]>
33 changes: 33 additions & 0 deletions rclc/include/rclc/executor.h
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,39 @@ rclc_executor_add_subscription(
rclc_callback_t callback,
rclc_executor_handle_invocation_t invocation);

/**
* Adds a subscription to an executor.
* * An error is returned, if {@link rclc_executor_t.handles} array is full.
* * The total number_of_subscriptions field of {@link rclc_executor_t.info}
* is incremented by one.
*
* <hr>
* Attribute | Adherence
* ------------------ | -------------
* Allocates Memory | No
* Thread-Safe | No
* Uses Atomics | No
* Lock-Free | Yes
*
* \param [inout] executor pointer to initialized executor
* \param [in] subscription pointer to an allocated subscription
* \param [in] msg pointer to an allocated message
* \param [in] callback function pointer to a callback
* \param [in] context type-erased ptr to additional callback context
* \param [in] invocation invocation type for the callback (ALWAYS or only ON_NEW_DATA)
* \return `RCL_RET_OK` if add-operation was successful
* \return `RCL_RET_INVALID_ARGUMENT` if any parameter is a null pointer (NULL context is ignored)
* \return `RCL_RET_ERROR` if any other error occured
*/
rcl_ret_t
rclc_executor_add_subscription_with_context(
rclc_executor_t * executor,
rcl_subscription_t * subscription,
void * msg,
rclc_subscription_callback_with_context_t callback,
void * context,
rclc_executor_handle_invocation_t invocation);

/**
* Adds a timer to an executor.
* * An error is returned, if {@link rclc_executor_t.handles} array is full.
Expand Down
14 changes: 12 additions & 2 deletions rclc/include/rclc/executor_handle.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,15 @@ typedef enum
/// Type definition for callback function.
typedef void (* rclc_callback_t)(const void *);

/// Type definition for subscription callback function
/// - incoming message
// typedef void (* rclc_subscription_callback_t)(const void *);

/// Type definition for subscription callback function
/// - incoming message
/// - additional callback context
typedef void (* rclc_subscription_callback_with_context_t)(const void *, void *);

/// Type definition for client callback function
/// - request message
/// - response message
Expand Down Expand Up @@ -114,8 +123,8 @@ typedef struct
/// only for service - ptr to response message
void * data_response_msg;

/// only for service - ptr to additional service context
void * service_context;
/// ptr to additional callback context
void * callback_context;

// TODO(jst3si) new type to be stored as data for
// service/client objects
Expand All @@ -129,6 +138,7 @@ typedef struct
/// Storage for callbacks
union {
rclc_callback_t callback;
rclc_subscription_callback_with_context_t subscription_callback_with_context;
rclc_service_callback_t service_callback;
rclc_service_callback_with_request_id_t service_callback_with_reqid;
rclc_service_callback_with_context_t service_callback_with_context;
Expand Down
81 changes: 75 additions & 6 deletions rclc/src/rclc/executor.c
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ rclc_executor_add_subscription(
executor->handles[executor->index].subscription = subscription;
executor->handles[executor->index].data = msg;
executor->handles[executor->index].callback = callback;
executor->handles[executor->index].callback_type = CB_WITHOUT_REQUEST_ID;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you want to change this? Default is CB_UNDEFINED

The type CB_WITH_REQUEST_ID and CB_WITHOUT_REQUEST_IDis currently used for services/clients.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also prefer CB_WITHOUT_CONTEXT

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The subscriptions without context need something in their callback_type that is well-defined, but has neither request-id nor context. CB_WITHOUT_CONTEXT sounds more generic, but is no more informative than CB_WITHOUT_REQUEST_ID, and would require another enum value.
(excuse me if this argument is already settled elsewhere, I'm catching up on the recent activity)

Copy link
Contributor

@JanStaschulat JanStaschulat Jul 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, but CB_WITHOUT_REQUEST_ID only makes sense for services and clients. CB_WITHOUT_CONTEXT is informative, that it is a normal callback. If you prefer another wording, how about CB_DEFAULT as a neutral enum value.

executor->handles[executor->index].invocation = invocation;
executor->handles[executor->index].initialized = true;

Expand All @@ -244,6 +245,56 @@ rclc_executor_add_subscription(
return ret;
}


rcl_ret_t
rclc_executor_add_subscription_with_context(
rclc_executor_t * executor,
rcl_subscription_t * subscription,
void * msg,
rclc_subscription_callback_with_context_t callback,
void * context,
rclc_executor_handle_invocation_t invocation)
{
RCL_CHECK_ARGUMENT_FOR_NULL(executor, RCL_RET_INVALID_ARGUMENT);
RCL_CHECK_ARGUMENT_FOR_NULL(subscription, RCL_RET_INVALID_ARGUMENT);
RCL_CHECK_ARGUMENT_FOR_NULL(msg, RCL_RET_INVALID_ARGUMENT);
RCL_CHECK_ARGUMENT_FOR_NULL(callback, RCL_RET_INVALID_ARGUMENT);
rcl_ret_t ret = RCL_RET_OK;
// array bound check
if (executor->index >= executor->max_handles) {
RCL_SET_ERROR_MSG("Buffer overflow of 'executor->handles'. Increase 'max_handles'");
return RCL_RET_ERROR;
}

// assign data fields
executor->handles[executor->index].type = SUBSCRIPTION;
executor->handles[executor->index].subscription = subscription;
executor->handles[executor->index].data = msg;
executor->handles[executor->index].subscription_callback_with_context = callback;
executor->handles[executor->index].callback_type = CB_WITH_CONTEXT;
executor->handles[executor->index].invocation = invocation;
executor->handles[executor->index].initialized = true;
executor->handles[executor->index].callback_context = context;

// increase index of handle array
executor->index++;

// invalidate wait_set so that in next spin_some() call the
// 'executor->wait_set' is updated accordingly
if (rcl_wait_set_is_valid(&executor->wait_set)) {
ret = rcl_wait_set_fini(&executor->wait_set);
if (RCL_RET_OK != ret) {
RCL_SET_ERROR_MSG("Could not reset wait_set in rclc_executor_add_subscription_with_context.");
return ret;
}
}

executor->info.number_of_subscriptions++;

RCUTILS_LOG_DEBUG_NAMED(ROS_PACKAGE_NAME, "Added a subscription.");
return ret;
}

rcl_ret_t
rclc_executor_add_timer(
rclc_executor_t * executor,
Expand Down Expand Up @@ -460,7 +511,7 @@ rclc_executor_add_service_with_context(
executor->handles[executor->index].callback_type = CB_WITH_CONTEXT;
executor->handles[executor->index].invocation = ON_NEW_DATA; // invoce when request came in
executor->handles[executor->index].initialized = true;
executor->handles[executor->index].service_context = context;
executor->handles[executor->index].callback_context = context;

// increase index of handle array
executor->index++;
Expand Down Expand Up @@ -923,10 +974,28 @@ _rclc_execute(rclc_executor_handle_t * handle)
if (invoke_callback) {
switch (handle->type) {
case SUBSCRIPTION:
if (handle->data_available) {
handle->callback(handle->data);
} else {
handle->callback(NULL);
switch (handle->callback_type) {
case CB_WITHOUT_REQUEST_ID:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be something like CB_WITHOUT_CONTEXT?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes.

CB_WITHOUT_REQUEST_ID comes from when request_id was added to services in Nov2020,
4bc9c13#diff-682b2c9ebab8a01f15a96d651ebc0e2aec2748f7abfafe288a48f9eb9fcc8786R48-R53
CB_WITHOUT_REQUEST_ID was treated like a default callback_type for the original api. It made sense when the request ID was the only special case, and only for services

I don't think it needs another callback-type, just a better name.

handle.callback_type is never checked without first checking handle.type, so I can also see an argument for eliminating handle.callback_type and extending handle.type to match 1:1 with all the types in the handle's function-pointer union:

union {
rclc_callback_t callback;
rclc_subscription_callback_with_context_t subscription_callback_with_context;
rclc_service_callback_t service_callback;
rclc_service_callback_with_request_id_t service_callback_with_reqid;
rclc_service_callback_with_context_t service_callback_with_context;
rclc_client_callback_t client_callback;
rclc_client_callback_with_request_id_t client_callback_with_reqid;
rclc_gc_callback_t gc_callback;
};

Copy link
Contributor

@JanStaschulat JanStaschulat Jul 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I agree. Initally, we only had services, subscriptions, clients. Now we have multiple types of callbacks for subscriptions, services and clients:

  • with request id/without request id (for services/clients),
  • with context, without context (for subscriptions/clients/services).

It would make sense to refactor handle->type and add e..g
SERVICE_WITH_REQ_ID
SERVICE_WITHOUT_REQ_ID
SERVICE_WITHOUT_CONTEXT
SERVICE_WITH_CONTEXT
SUBSCRIPTION_WITH_CONTEXT
SUBSCRIPTION_WITHOUT_CONTEXT
same for CLIENTS ...
(quite a number of types ...)

the request-id is currently not used in services with context.
@norro @pablogs9 @BrettRD I propose to keep the structure like this in this pull request. Opened a new issue for discussing how to simplify callback-type and handle-type variables.
Issue: #116

@norro. It looks like that the implementation of the client callback with context is not properly called: it only calls the client callback without context:

handle->client_callback(handle->data);

You never use a client callback with context?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added an issue for that :#115

if (handle->data_available) {
handle->callback(handle->data);
} else {
handle->callback(NULL);
}
break;
case CB_WITH_CONTEXT:
if (handle->data_available) {
handle->subscription_callback_with_context(
handle->data,
handle->callback_context);
} else {
handle->subscription_callback_with_context(
NULL,
handle->callback_context);
}
break;
default:
PRINT_RCLC_ERROR(rclc_execute, unknown_callback_type);
break;
}
break;

Expand Down Expand Up @@ -955,7 +1024,7 @@ _rclc_execute(rclc_executor_handle_t * handle)
handle->service_callback_with_context(
handle->data,
handle->data_response_msg,
handle->service_context);
handle->callback_context);
break;
default:
PRINT_RCLC_ERROR(rclc_execute, unknown_callback_type);
Expand Down
2 changes: 1 addition & 1 deletion rclc/src/rclc/executor_handle.c
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ rclc_executor_handle_init(

handle->data = NULL;
handle->data_response_msg = NULL;
handle->service_context = NULL;
handle->callback_context = NULL;

handle->callback = NULL;
// because of union structure:
Expand Down
115 changes: 115 additions & 0 deletions rclc/test/rclc/test_executor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,20 @@ void int32_callback5(const void * msgin)
}
}

void int32_callback_with_context(const void * msgin, void * context)
{
const std_msgs__msg__Int32 * msg = (const std_msgs__msg__Int32 *)msgin;
if (msg == NULL) {
printf("(int32_callback_with_context): msg is NULL\n");
}
if (context == NULL) {
printf("(int32_callback_with_context): context is NULL\n");
} else {
// This side effect allows the test to check that the subscription received its context
int32_t * sub_context_value = reinterpret_cast<int32_t *>( context );
*sub_context_value = msg->data;
}
}

void service_callback(const void * req_msg, void * resp_msg)
{
Expand Down Expand Up @@ -697,6 +711,66 @@ TEST_F(TestDefaultExecutor, executor_add_subscription) {
EXPECT_EQ(RCL_RET_OK, rc) << rcl_get_error_string().str;
}

TEST_F(TestDefaultExecutor, executor_add_subscription_with_context) {
rcl_ret_t rc;
rclc_executor_t executor;
// test with normal arguemnt and NULL pointers as arguments
rc = rclc_executor_init(&executor, &this->context, 10, this->allocator_ptr);
EXPECT_EQ(RCL_RET_OK, rc) << rcl_get_error_string().str;

int32_t sub_context_value = 0;
void * sub_context_ptr = reinterpret_cast<void *>( &sub_context_value );

// normal case
rc = rclc_executor_add_subscription_with_context(
&executor, &this->sub1, &this->sub1_msg,
&int32_callback_with_context, sub_context_ptr, ON_NEW_DATA);
EXPECT_EQ(RCL_RET_OK, rc) << rcl_get_error_string().str;
size_t num_subscriptions = 1;
EXPECT_EQ(executor.info.number_of_subscriptions, num_subscriptions) <<
"number of subscriptions is expected to be one";

// test NULL pointer for executor
rc = rclc_executor_add_subscription_with_context(
NULL, &this->sub1, &this->sub1_msg, &int32_callback_with_context,
sub_context_ptr, ON_NEW_DATA);
EXPECT_EQ(RCL_RET_INVALID_ARGUMENT, rc) << rcl_get_error_string().str;
rcutils_reset_error();
EXPECT_EQ(executor.info.number_of_subscriptions, num_subscriptions) <<
"number of subscriptions is expected to be one";

// test NULL pointer for subscription
rc = rclc_executor_add_subscription_with_context(
&executor, NULL, &this->sub1_msg, &int32_callback_with_context,
sub_context_ptr, ON_NEW_DATA);
EXPECT_EQ(RCL_RET_INVALID_ARGUMENT, rc) << rcl_get_error_string().str;
rcutils_reset_error();
EXPECT_EQ(executor.info.number_of_subscriptions, num_subscriptions) <<
"number of subscriptions is expected to be one";

// test NULL pointer for message
rc = rclc_executor_add_subscription_with_context(
&executor, &this->sub1, NULL, &int32_callback_with_context,
sub_context_ptr, ON_NEW_DATA);
EXPECT_EQ(RCL_RET_INVALID_ARGUMENT, rc) << rcl_get_error_string().str;
rcutils_reset_error();
EXPECT_EQ(executor.info.number_of_subscriptions, num_subscriptions) <<
"number of subscriptions is expected to be one";

// test NULL pointer for callback
rc = rclc_executor_add_subscription_with_context(
&executor, &this->sub1, &this->sub1_msg, NULL,
sub_context_ptr, ON_NEW_DATA);
EXPECT_EQ(RCL_RET_INVALID_ARGUMENT, rc) << rcl_get_error_string().str;
rcutils_reset_error();
EXPECT_EQ(executor.info.number_of_subscriptions, num_subscriptions) <<
"number of subscriptions is expected to be one";

// tear down
rc = rclc_executor_fini(&executor);
EXPECT_EQ(RCL_RET_OK, rc) << rcl_get_error_string().str;
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would need a test case where you add a subscription with a context, publish data and check if the data was correctly handled, like this one for service_with_context

TEST_F(TestDefaultExecutor, executor_test_service_with_context) {

or a simplified version of this test case spin_some_sequential_execution

TEST_F(TestDefaultExecutor, spin_some_sequential_execution) {

in your test case you only need one subsction. e.g. add_subscription to executor , publish a topic, wait for some time, call spin_some and then copy the received data and the context data in the subscription_with_context_callback to a global variable. These ones you can then check in the test case.

Would be great, if you could add an example in rclc_examples repository that shows how to use this feature. You could use this one as a base-line
https://github.com/ros2/rclc/blob/master/rclc_examples/src/example_executor_convenience.c

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test added, Example added.
I don't think I can reliably test that context is not passed to a regular subscription; That would need a very carefully crafted stack canary, and it would be quite brittle to nearby changes.

TEST_F(TestDefaultExecutor, executor_add_subscription_too_many) {
rcl_ret_t rc;
rclc_executor_t executor;
Expand Down Expand Up @@ -2447,6 +2521,47 @@ TEST_F(TestDefaultExecutor, executor_test_service_with_context) {
example_interfaces__srv__AddTwoInts_Response__fini(&cli_resp);
}

TEST_F(TestDefaultExecutor, executor_test_subscription_with_context) {
// This unit test tests, that a subscription with context receives the correct context pointer
rcl_ret_t rc;
rclc_executor_t executor;
executor = rclc_executor_get_zero_initialized_executor();
rc = rclc_executor_init(&executor, &this->context, 10, this->allocator_ptr);
EXPECT_EQ(RCL_RET_OK, rc) << rcl_get_error_string().str;

// prepare a context pointer carrying a value that differs from the published value
int32_t sub_context_value = 0;
void * sub_context_ptr = reinterpret_cast<void *>( &sub_context_value );

std_msgs__msg__Int32__init(&this->pub1_msg);
this->pub1_msg.data = 42;

// create a subscription on the same topic as our publisher
EXPECT_EQ(executor.info.number_of_subscriptions, (size_t) 0) <<
"number of subscriptions was not initialised at zero";

rc = rclc_executor_add_subscription_with_context(
&executor, &this->sub1, &this->sub1_msg,
&int32_callback_with_context, sub_context_ptr, ON_NEW_DATA);
EXPECT_EQ(RCL_RET_OK, rc) << rcl_get_error_string().str;
EXPECT_EQ(executor.info.number_of_subscriptions, (size_t) 1) <<
"number of subscriptions is expected to be one";

// run the callback
rc = rcl_publish(&this->pub1, &this->pub1_msg, nullptr);
EXPECT_EQ(RCL_RET_OK, rc) << " pub1 not published";
std::this_thread::sleep_for(rclc_test_sleep_time);
rclc_executor_spin_some(&executor, rclc_test_timeout_ns);

// check the side effect
EXPECT_EQ(this->pub1_msg.data, sub_context_value) <<
"subscription did not alter context value";

// tear down
rc = rclc_executor_fini(&executor);
EXPECT_EQ(RCL_RET_OK, rc) << rcl_get_error_string().str;
}

TEST_F(TestDefaultExecutor, executor_test_guard_condition) {
// Test guard_condition.
rcl_ret_t rc;
Expand Down
4 changes: 4 additions & 0 deletions rclc_examples/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,17 @@ ament_target_dependencies(example_client_node rcl rclc example_interfaces)
add_executable(example_lifecycle_node src/example_lifecycle_node.c)
ament_target_dependencies(example_lifecycle_node rcutils rcl rcl_lifecycle rclc rclc_lifecycle lifecycle_msgs)

add_executable(example_sub_context src/example_sub_context.c)
ament_target_dependencies(example_sub_context rcl rclc std_msgs)

install(TARGETS
example_executor
example_executor_convenience
example_executor_trigger
example_lifecycle_node
example_service_node
example_client_node
example_sub_context
DESTINATION lib/${PROJECT_NAME}
)

Expand Down
Loading