diff --git a/rcl/include/rcl/rcl.h b/rcl/include/rcl/rcl.h index 4c27d73..21d5f04 100644 --- a/rcl/include/rcl/rcl.h +++ b/rcl/include/rcl/rcl.h @@ -46,11 +46,17 @@ extern "C" * be ignored. * If argc is 0 and argv is NULL no parameters will be parsed. * + * This function allocates heap memory. + * This function is not thread-safe. + * This function is lock-free so long as the C11's stdatomic.h function + * atomic_is_lock_free() returns true for atomic_uint_least64_t. + * * \param[in] argc number of strings in argv * \param[in] argv command line arguments; rcl specific arguments are removed * \param[in] allocator allocator to be used during rcl_init and rcl_shutdown * \return RCL_RET_OK if initialization is successful, or * RCL_RET_ALREADY_INIT if rcl_init has already been called, or + * RCL_RET_BAD_ALLOC if allocating memory failed, or * RCL_RET_ERROR if an unspecified error occurs. */ RCL_PUBLIC @@ -72,21 +78,39 @@ rcl_init(int argc, char ** argv, rcl_allocator_t allocator); * - No new work (executing callbacks) will be done in executors. * - Currently running work in executors will be finished. * - * \return RCL_RET_OK if shutdown is successful, otherwise RCL_RET_ERROR or - * RCL_RET_NOT_INIT if rcl_init has not yet been called + * This function does not allocate heap memory. + * This function is thread-safe, except with rcl_init(). + * This function is lock-free so long as the C11's stdatomic.h function + * atomic_is_lock_free() returns true for atomic_uint_least64_t. + * + * \return RCL_RET_OK if the shutdown was completed successfully, or + * RCL_RET_NOT_INIT if rcl is not currently initialized, or + * RCL_RET_ERROR if an unspecified error occur. */ RCL_PUBLIC rcl_ret_t rcl_shutdown(); /// Returns an uint64_t number that is unique for the latest rcl_init call. -/* If called before rcl_init or after rcl_shutdown then 0 will be returned. */ +/* If called before rcl_init or after rcl_shutdown then 0 will be returned. + * + * This function does not allocate memory. + * This function is thread-safe. + * This function is lock-free so long as the C11's stdatomic.h function + * atomic_is_lock_free() returns true for atomic_uint_least64_t. + * + * \return a unique id specific to this rcl instance, or 0 if not initialized. + */ RCL_PUBLIC uint64_t rcl_get_instance_id(); -/// Return true until rcl_shutdown is called, then false. -/* This function is thread safe. */ +/// Return true if rcl is currently initialized, otherwise false. +/* This function does not allocate memory. + * This function is thread-safe. + * This function is lock-free so long as the C11's stdatomic.h function + * atomic_is_lock_free() returns true for atomic_uint_least64_t. + */ RCL_PUBLIC bool rcl_ok(); diff --git a/rcl/src/rcl/rcl.c b/rcl/src/rcl/rcl.c index 839a81c..13ec6a8 100644 --- a/rcl/src/rcl/rcl.c +++ b/rcl/src/rcl/rcl.c @@ -21,6 +21,7 @@ extern "C" #include +#include "./common.h" #include "./stdatomic_helper.h" #include "rcl/error_handling.h" @@ -38,11 +39,15 @@ __clean_up_init() size_t i; for (i = 0; i < __rcl_argc; ++i) { if (__rcl_argv[i]) { + // Use the old allocator. __rcl_allocator.deallocate(__rcl_argv[i], __rcl_allocator.state); } } + // Use the old allocator. __rcl_allocator.deallocate(__rcl_argv, __rcl_allocator.state); } + __rcl_argc = 0; + __rcl_argv = NULL; rcl_atomic_store(&__rcl_instance_id, 0); rcl_atomic_store(&__rcl_is_initialized, false); } @@ -50,16 +55,28 @@ __clean_up_init() rcl_ret_t rcl_init(int argc, char ** argv, rcl_allocator_t allocator) { + rcl_ret_t fail_ret = RCL_RET_ERROR; + if (argc > 0) { + RCL_CHECK_ARGUMENT_FOR_NULL(argv, RCL_RET_INVALID_ARGUMENT); + } + RCL_CHECK_FOR_NULL_WITH_MSG( + allocator.allocate, + "invalid allocator, allocate not set", return RCL_RET_INVALID_ARGUMENT); + RCL_CHECK_FOR_NULL_WITH_MSG( + allocator.deallocate, + "invalid allocator, deallocate not set", return RCL_RET_INVALID_ARGUMENT); if (rcl_atomic_exchange_bool(&__rcl_is_initialized, true)) { RCL_SET_ERROR_MSG("rcl_init called while already initialized"); return RCL_RET_ALREADY_INIT; } // TODO(wjwwood): Remove rcl specific command line arguments. // For now just copy the argc and argv. + __rcl_allocator = allocator; // Set the new allocator. __rcl_argc = argc; __rcl_argv = (char **)__rcl_allocator.allocate(sizeof(char *) * argc, __rcl_allocator.state); if (!__rcl_argv) { RCL_SET_ERROR_MSG("allocation failed"); + fail_ret = RCL_RET_BAD_ALLOC; goto fail; } memset(__rcl_argv, 0, sizeof(char **) * argc); @@ -78,7 +95,7 @@ rcl_init(int argc, char ** argv, rcl_allocator_t allocator) return RCL_RET_OK; fail: __clean_up_init(); - return RCL_RET_ERROR; + return fail_ret; } rcl_ret_t diff --git a/rcl/test/CMakeLists.txt b/rcl/test/CMakeLists.txt index 11f393a..83f96b6 100644 --- a/rcl/test/CMakeLists.txt +++ b/rcl/test/CMakeLists.txt @@ -79,4 +79,16 @@ if(AMENT_ENABLE_TESTING) endif() target_link_libraries(test_common ${PROJECT_NAME} ${extra_test_libraries}) endif() + + ament_add_gtest(test_rcl rcl/test_rcl.cpp ENV ${extra_memory_tools_env}) + if(TARGET test_rcl) + target_include_directories(test_rcl PUBLIC + ${rcl_interfaces_INCLUDE_DIRS} + ${rmw_INCLUDE_DIRS} + ) + if(NOT WIN32) + set_target_properties(test_rcl PROPERTIES COMPILE_FLAGS "-std=c++11") + endif() + target_link_libraries(test_rcl ${PROJECT_NAME} ${extra_test_libraries}) + endif() endif() diff --git a/rcl/test/memory_tools_common.cpp b/rcl/test/memory_tools_common.cpp index 30949b4..a2672ed 100644 --- a/rcl/test/memory_tools_common.cpp +++ b/rcl/test/memory_tools_common.cpp @@ -63,7 +63,7 @@ custom_malloc(size_t size) } void * memory = malloc(size); MALLOC_PRINTF( - "malloc expected(%s): %p %zu\n", malloc_expected ? "true " : "false", memory, size); + "malloc expected(%s): %p %llu\n", malloc_expected ? "true " : "false", memory, size); return memory; } @@ -103,7 +103,7 @@ custom_realloc(void * memory_in, size_t size) } void * memory = realloc(memory_in, size); MALLOC_PRINTF( - "realloc expected(%s): %p %p %zu\n", + "realloc expected(%s): %p %p %llu\n", malloc_expected ? "true " : "false", memory_in, memory, size); return memory; } diff --git a/rcl/test/rcl/test_rcl.cpp b/rcl/test/rcl/test_rcl.cpp new file mode 100644 index 0000000..b5517a1 --- /dev/null +++ b/rcl/test/rcl/test_rcl.cpp @@ -0,0 +1,180 @@ +// Copyright 2015 Open Source Robotics Foundation, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +#include "rcl/rcl.h" + +#include "../memory_tools.hpp" +#include "rcl/error_handling.h" + +class TestRCLFixture : public::testing::Test +{ +public: + void SetUp() + { + set_on_unepexcted_malloc_callback([]() {ASSERT_FALSE(true) << "UNEXPECTED MALLOC";}); + set_on_unepexcted_realloc_callback([]() {ASSERT_FALSE(true) << "UNEXPECTED REALLOC";}); + set_on_unepexcted_free_callback([]() {ASSERT_FALSE(true) << "UNEXPECTED FREE";}); + start_memory_checking(); + } + + void TearDown() + { + assert_no_malloc_end(); + assert_no_realloc_end(); + assert_no_free_end(); + stop_memory_checking(); + set_on_unepexcted_malloc_callback(nullptr); + set_on_unepexcted_realloc_callback(nullptr); + set_on_unepexcted_free_callback(nullptr); + } +}; + +void * +failing_malloc(size_t size, void * state) +{ + (void)(size); + (void)(state); + return nullptr; +} + +void * +failing_realloc(void * pointer, size_t size, void * state) +{ + (void)(pointer); + (void)(size); + (void)(state); + return nullptr; +} + +void +failing_free(void * pointer, void * state) +{ + (void)pointer; + (void)state; + return; +} + +struct FakeTestArgv +{ + FakeTestArgv() : argc(2) + { + this->argv = (char **)malloc(2 * sizeof(char *)); + if (!this->argv) { + throw std::bad_alloc(); + } + this->argv[0] = (char *)malloc(10 * sizeof(char)); + sprintf(this->argv[0], "foo"); + this->argv[1] = (char *)malloc(10 * sizeof(char)); + sprintf(this->argv[1], "bar"); + } + + ~FakeTestArgv() + { + if (this->argv) { + if (this->argv > 0) { + size_t unsigned_argc = this->argc; + for (size_t i = 0; i < unsigned_argc; --i) { + free(this->argv[i]); + } + } + } + free(this->argv); + } + + int argc; + char ** argv; +}; + +/* Tests the rcl_init() and rcl_shutdown() functions. + */ +TEST_F(TestRCLFixture, test_rcl_init_and_shutdown) +{ + rcl_ret_t ret; + // A shutdown before any init has been called should fail. + ret = rcl_shutdown(); + EXPECT_EQ(RCL_RET_NOT_INIT, ret); + rcl_reset_error(); + ASSERT_EQ(false, rcl_ok()); + // If argc is not 0, but argv is, it should be an invalid argument. + ret = rcl_init(42, nullptr, rcl_get_default_allocator()); + EXPECT_EQ(RCL_RET_INVALID_ARGUMENT, ret); + rcl_reset_error(); + ASSERT_EQ(false, rcl_ok()); + // If either the allocate or deallocate function pointers are not set, it should be invalid arg. + rcl_allocator_t invalid_allocator = rcl_get_default_allocator(); + invalid_allocator.allocate = nullptr; + ret = rcl_init(0, nullptr, invalid_allocator); + EXPECT_EQ(RCL_RET_INVALID_ARGUMENT, ret); + rcl_reset_error(); + ASSERT_EQ(false, rcl_ok()); + invalid_allocator.allocate = rcl_get_default_allocator().allocate; + invalid_allocator.deallocate = nullptr; + ret = rcl_init(0, nullptr, invalid_allocator); + EXPECT_EQ(RCL_RET_INVALID_ARGUMENT, ret); + rcl_reset_error(); + ASSERT_EQ(false, rcl_ok()); + // If the malloc call fails (with some valid arguments to copy), it should be a bad alloc. + { + FakeTestArgv test_args; + rcl_allocator_t failing_allocator = rcl_get_default_allocator(); + failing_allocator.allocate = &failing_malloc; + failing_allocator.deallocate = failing_free; + failing_allocator.reallocate = failing_realloc; + ret = rcl_init(test_args.argc, test_args.argv, failing_allocator); + EXPECT_EQ(RCL_RET_BAD_ALLOC, ret); + rcl_reset_error(); + ASSERT_EQ(false, rcl_ok()); + } + // If argc is 0 and argv is nullptr and the allocator is valid, it should succeed. + ret = rcl_init(0, nullptr, rcl_get_default_allocator()); + EXPECT_EQ(RCL_RET_OK, ret); + ASSERT_EQ(true, rcl_ok()); + // Then shutdown should work. + ret = rcl_shutdown(); + EXPECT_EQ(ret, RCL_RET_OK); + ASSERT_EQ(false, rcl_ok()); + // Valid argc/argv values and a valid allocator should succeed. + { + FakeTestArgv test_args; + ret = rcl_init(test_args.argc, test_args.argv, rcl_get_default_allocator()); + EXPECT_EQ(RCL_RET_OK, ret); + ASSERT_EQ(true, rcl_ok()); + } + // Then shutdown should work. + ret = rcl_shutdown(); + EXPECT_EQ(RCL_RET_OK, ret); + ASSERT_EQ(false, rcl_ok()); + // A repeat call to shutdown should not work. + ret = rcl_shutdown(); + EXPECT_EQ(RCL_RET_NOT_INIT, ret); + rcl_reset_error(); + ASSERT_EQ(false, rcl_ok()); + // Repeat, but valid, calls to rcl_init() should fail. + { + FakeTestArgv test_args; + ret = rcl_init(test_args.argc, test_args.argv, rcl_get_default_allocator()); + EXPECT_EQ(RCL_RET_OK, ret); + ASSERT_EQ(true, rcl_ok()); + ret = rcl_init(test_args.argc, test_args.argv, rcl_get_default_allocator()); + EXPECT_EQ(RCL_RET_ALREADY_INIT, ret); + rcl_reset_error(); + ASSERT_EQ(true, rcl_ok()); + } + // But shutdown should still work. + ret = rcl_shutdown(); + EXPECT_EQ(ret, RCL_RET_OK); + ASSERT_EQ(false, rcl_ok()); +}