Skip to content

Commit

Permalink
dynamic_modules: adds initial object loading logic (#35550)
Browse files Browse the repository at this point in the history
Commit Message: dynamic_modules: adds initial object loading logic
Additional Description:
This is the very first commit of the dynamic loading feature discussed
among community
members. This is the effort to upstream the playground repository 
https://github.com/mathetake/envoy-dynamic-modules as an Envoy core
extension.
Series of commits will follow this little by little.

#2053, #24230, #32502

Risk Level: N/A (not compiled into the final build yet)
Testing: unit
Docs Changes: N/A
Release Notes: N/A 
Platform Specific Features: N/A
[Optional Runtime guard:]
[Optional Fixes #Issue] 
[Optional Fixes commit #PR or SHA]
[Optional Deprecated:]
[Optional [API
Considerations](https://github.com/envoyproxy/envoy/blob/main/api/review_checklist.md):]

---------

Signed-off-by: Takeshi Yoneda <[email protected]>
  • Loading branch information
mathetake authored Aug 6, 2024
1 parent eb2fbdc commit 6bdb8ff
Show file tree
Hide file tree
Showing 10 changed files with 255 additions and 2 deletions.
5 changes: 3 additions & 2 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,7 @@ extensions/filters/http/oauth2 @derekargueta @mattklein123
/*/extensions/health_checkers/grpc @zuercher @botengyao
/*/extensions/health_checkers/http @zuercher @botengyao
/*/extensions/health_checkers/tcp @zuercher @botengyao
/*/extensions/health_checkers/common @zuercher @botengyao
# Health check event sinks
/*/extensions/health_check/event_sinks/file @botengyao @yanavlasov
# IP Geolocation
Expand All @@ -349,8 +350,8 @@ extensions/filters/http/oauth2 @derekargueta @mattklein123
/*/extensions/filters/http/match_delegate @wbpcode @jstraceski @tyxia
# Generic proxy and related extensions
/*/extensions/filters/network/generic_proxy/ @wbpcode @soulxu

/*/extensions/health_checkers/common @zuercher @botengyao
# Dynamic Modules
/*/extensions/dynamic_modules @mattklein123 @mathetake @marc-barry

# HTTP credential injector
/*/extensions/filters/http/credential_injector @zhaohuabing @kyessenov
Expand Down
22 changes: 22 additions & 0 deletions source/extensions/dynamic_modules/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
load(
"//bazel:envoy_build_system.bzl",
"envoy_cc_library",
"envoy_extension_package",
)

licenses(["notice"]) # Apache 2

envoy_extension_package()

envoy_cc_library(
name = "dynamic_modules_lib",
srcs = [
"dynamic_modules.cc",
],
hdrs = [
"dynamic_modules.h",
],
deps = [
"//envoy/common:exception_lib",
],
)
44 changes: 44 additions & 0 deletions source/extensions/dynamic_modules/dynamic_modules.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#include "source/extensions/dynamic_modules/dynamic_modules.h"

#include <dlfcn.h>

#include <filesystem>
#include <string>

#include "envoy/common/exception.h"

namespace Envoy {
namespace Extensions {
namespace DynamicModules {

absl::StatusOr<DynamicModuleSharedPtr> newDynamicModule(const absl::string_view object_file_path,
const bool do_not_close) {
// RTLD_LOCAL is always needed to avoid collisions between multiple modules.
// RTLD_LAZY is required for not only performance but also simply to load the module, otherwise
// dlopen results in Invalid argument.
int mode = RTLD_LOCAL | RTLD_LAZY;
if (do_not_close) {
mode |= RTLD_NODELETE;
}

const std::filesystem::path file_path_absolute = std::filesystem::absolute(object_file_path);
void* handle = dlopen(file_path_absolute.c_str(), mode);
if (handle == nullptr) {
return absl::InvalidArgumentError(
absl::StrCat("Failed to load dynamic module: ", object_file_path, " : ", dlerror()));
}
return std::make_shared<DynamicModule>(handle);
}

DynamicModule::~DynamicModule() { dlclose(handle_); }

void* DynamicModule::getSymbol(const absl::string_view symbol_ref) const {
// TODO(mathetake): maybe we should accept null-terminated const char* instead of string_view to
// avoid unnecessary copy because it is likely that this is only called for a constant string,
// though this is not a performance critical path.
return dlsym(handle_, std::string(symbol_ref).c_str());
}

} // namespace DynamicModules
} // namespace Extensions
} // namespace Envoy
65 changes: 65 additions & 0 deletions source/extensions/dynamic_modules/dynamic_modules.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#pragma once

#include <memory>
#include <string>

#include "absl/status/statusor.h"
#include "absl/strings/string_view.h"

namespace Envoy {
namespace Extensions {
namespace DynamicModules {

/**
* A class for loading and managing dynamic modules. This corresponds to a single dlopen handle.
* When the DynamicModule object is destroyed, the dlopen handle is closed.
*
* This class is supposed to be initialized once in the main thread and can be shared with other
* threads.
*/
class DynamicModule {
public:
DynamicModule(void* handle) : handle_(handle) {}
~DynamicModule();

/**
* Get a function pointer from the dynamic module with a specific type.
* @param T the function pointer type to cast the symbol to.
* @param symbol_ref the symbol to look up.
* @return the symbol if found, otherwise nullptr.
*/
template <typename T> T getFunctionPointer(const absl::string_view symbol_ref) const {
static_assert(std::is_pointer<T>::value &&
std::is_function<typename std::remove_pointer<T>::type>::value,
"T must be a function pointer type");
return reinterpret_cast<T>(getSymbol(symbol_ref));
}

private:
/**
* Get a symbol from the dynamic module.
* @param symbol_ref the symbol to look up.
* @return the symbol if found, otherwise nullptr.
*/
void* getSymbol(const absl::string_view symbol_ref) const;

// The raw dlopen handle that can be used to look up symbols.
void* handle_;
};

using DynamicModuleSharedPtr = std::shared_ptr<DynamicModule>;

/**
* Creates a new DynamicModule.
* @param object_file_path the path to the object file to load.
* @param do_not_close if true, the dlopen will be called with RTLD_NODELETE, so the loaded object
* will not be destroyed. This is useful when an object has some global state that should not be
* terminated. For example, c-shared objects compiled by Go doesn't support dlclose
* https://github.com/golang/go/issues/11100.
*/
absl::StatusOr<DynamicModuleSharedPtr> newDynamicModule(const absl::string_view object_file_path,
const bool do_not_close);

} // namespace DynamicModules
} // namespace Extensions
} // namespace Envoy
22 changes: 22 additions & 0 deletions test/extensions/dynamic_modules/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
load(
"//bazel:envoy_build_system.bzl",
"envoy_cc_test",
"envoy_package",
)

licenses(["notice"]) # Apache 2

envoy_package()

envoy_cc_test(
name = "dynamic_modules_test",
srcs = ["dynamic_modules_test.cc"],
data = [
"//test/extensions/dynamic_modules/test_data:no_op",
],
deps = [
"//source/extensions/dynamic_modules:dynamic_modules_lib",
"//test/test_common:environment_lib",
"//test/test_common:utility_lib",
],
)
68 changes: 68 additions & 0 deletions test/extensions/dynamic_modules/dynamic_modules_test.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#include <memory>

#include "envoy/common/exception.h"

#include "source/extensions/dynamic_modules/dynamic_modules.h"

#include "test/test_common/environment.h"
#include "test/test_common/utility.h"

#include "gtest/gtest.h"

namespace Envoy {
namespace Extensions {
namespace DynamicModules {

// This loads a shared object file from the test_data directory.
std::string testSharedObjectPath(std::string name) {
return TestEnvironment::substitute(
"{{ test_rundir }}/test/extensions/dynamic_modules/test_data/") +
"lib" + name + ".so";
}

TEST(DynamicModuleTest, InvalidPath) {
absl::StatusOr<DynamicModuleSharedPtr> result = newDynamicModule("invalid_name", false);
EXPECT_FALSE(result.ok());
EXPECT_EQ(result.status().code(), absl::StatusCode::kInvalidArgument);
}

TEST(DynamicModuleTest, LoadNoOp) {
using GetSomeVariableFuncType = int (*)();
absl::StatusOr<DynamicModuleSharedPtr> module =
newDynamicModule(testSharedObjectPath("no_op"), false);
EXPECT_TRUE(module.ok());
const auto getSomeVariable =
module->get()->getFunctionPointer<GetSomeVariableFuncType>("getSomeVariable");
EXPECT_EQ(getSomeVariable(), 1);
EXPECT_EQ(getSomeVariable(), 2);
EXPECT_EQ(getSomeVariable(), 3);

// Release the module, and reload it.
module->reset();
module =
newDynamicModule(testSharedObjectPath("no_op"), true); // This time, do not close the module.
EXPECT_TRUE(module.ok());

// This module must be reloaded and the variable must be reset.
const auto getSomeVariable2 =
(module->get()->getFunctionPointer<GetSomeVariableFuncType>("getSomeVariable"));
EXPECT_NE(getSomeVariable2, nullptr);
EXPECT_EQ(getSomeVariable2(), 1); // Start from 1 again.
EXPECT_EQ(getSomeVariable2(), 2);
EXPECT_EQ(getSomeVariable2(), 3);

// Release the module, and reload it.
module->reset();
module = newDynamicModule(testSharedObjectPath("no_op"), false);
EXPECT_TRUE(module.ok());

// This module must be the already loaded one, and the variable must be kept.
const auto getSomeVariable3 =
module->get()->getFunctionPointer<GetSomeVariableFuncType>("getSomeVariable");
EXPECT_NE(getSomeVariable3, nullptr);
EXPECT_EQ(getSomeVariable3(), 4); // Start from 4.
}

} // namespace DynamicModules
} // namespace Extensions
} // namespace Envoy
7 changes: 7 additions & 0 deletions test/extensions/dynamic_modules/test_data/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
load("//test/extensions/dynamic_modules/test_data:test_data.bzl", "test_program")

licenses(["notice"]) # Apache 2

package(default_visibility = ["//visibility:public"])

test_program(name = "no_op")
5 changes: 5 additions & 0 deletions test/extensions/dynamic_modules/test_data/no_op.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
int getSomeVariable() {
static int some_variable = 0;
some_variable++;
return some_variable;
}
14 changes: 14 additions & 0 deletions test/extensions/dynamic_modules/test_data/test_data.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
load("@rules_cc//cc:defs.bzl", "cc_library")

# This declares a cc_library target that is used to build a shared library.
# name + ".c" is the source file that is compiled to create the shared library.
def test_program(name):
cc_library(
name = name,
srcs = [name + ".c"],
linkopts = [
"-shared",
"-fPIC",
],
linkstatic = False,
)
5 changes: 5 additions & 0 deletions tools/spelling/spelling_dictionary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ OWS
Preconnecting
RCVBUF
RTCP
RTLD
RTP
SOH
SPC
Expand Down Expand Up @@ -306,6 +307,7 @@ NOAUTH
NOCHECKRESP
NODATA
NODELAY
NODELETE
NOLINT
NOLINTNEXTLINE
NONAME
Expand Down Expand Up @@ -721,6 +723,9 @@ dgst
dir
dirname
djb
dlclose
dlopen
dlsym
downcalls
downcasted
downcased
Expand Down

0 comments on commit 6bdb8ff

Please sign in to comment.