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

dynamic_modules: HTTP filter config implementation #37070

Merged
merged 16 commits into from
Nov 21, 2024
77 changes: 70 additions & 7 deletions source/extensions/dynamic_modules/abi.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,46 @@ extern "C" {
// -----------------------------------------------------------------------------
//
// Types used in the ABI. The name of a type must be prefixed with "envoy_dynamic_module_type_".
// Types with "_module_ptr" suffix are pointers owned by the module, i.e. memory space allocated by
// the module. Types with "_envoy_ptr" suffix are pointers owned by Envoy, i.e. memory space
// allocated by Envoy.

/**
* envoy_dynamic_module_type_abi_version represents a null-terminated string that contains the ABI
* version of the dynamic module. This is used to ensure that the dynamic module is built against
* the compatible version of the ABI.
* envoy_dynamic_module_type_abi_version_envoy_ptr represents a null-terminated string that
* contains the ABI version of the dynamic module. This is used to ensure that the dynamic module is
* built against the compatible version of the ABI.
*
* OWNERSHIP: Envoy owns the pointer.
*/
typedef const char* // NOLINT(modernize-use-using)
envoy_dynamic_module_type_abi_version_envoy_ptr;

/**
* envoy_dynamic_module_type_http_filter_config_envoy_ptr is a raw pointer to
* the DynamicModuleHttpFilterConfig class in Envoy. This is passed to the module when
* creating a new in-module HTTP filter configuration and used to access the HTTP filter-scoped
* information such as metadata, metrics, etc.
*
* This has 1:1 correspondence with envoy_dynamic_module_type_http_filter_config_module_ptr in
* the module.
*
* OWNERSHIP: Envoy owns the pointer.
*/
typedef const void* // NOLINT(modernize-use-using)
envoy_dynamic_module_type_http_filter_config_envoy_ptr;
Comment on lines +50 to +51
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure I understand the purpose of passing void* here if the user has to have the actual C++ definition. Is this just to avoid any C++ leaking into the ABI itself?

Copy link
Member Author

Choose a reason for hiding this comment

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

That's a valid question. The users (modules) do not need to understand the C++ definition, but instead users can just call the corresponding ABI functions with this opaque pointers. For example, one notable example about filter-config level ABI function is "creating metrics". Then possible function signature for that ABI would look like

envoy_dynamic_module_type_counter_metric_envoy_ptr envoy_dynamic_module_define_counter_metric(
    envoy_dynamic_module_type_http_filter_config_envoy_ptr envoy_filter_config_ptr,
    const char* name,
    int name_length,
);

where the module->Envoy ABI function has this opaque pointer as a first argument. Then on the Envoy side, we can provide the functionality

envoy_dynamic_module_type_counter_metric_envoy_ptr envoy_dynamic_module_define_counter_metric(
    envoy_dynamic_module_type_http_filter_config_envoy_ptr envoy_filter_config_ptr,
    const char* name,
    int name_length,
) {
  const std::string_view name_view(static_cast<const char*>(name), name_length);
  auto scope = static_cast<DynamicModuleHttpFilterConfig*>(envoy_filter_config_ptr)->stat_scope_;
  Stats::StatNameManagedStorage storage(name_view, scope->symbolTable());
  Stats::StatName stat_name = storage.statName();
  Stats::Counter& counter = scope->counterFromStatName(stat_name);
  return &counter;
}

without leaking the implementation details of this envoy side "HttpFilterConfig" class into dynamic modules.

Apologies this is difficult to see the rationale behind this pointer passing without the concrete impl example like this. Later when I add these ABI functions, then we will be able to see the concrete picture more clearly. Hope this helps!


/**
* envoy_dynamic_module_type_http_filter_config_module_ptr is a pointer to an in-module HTTP
* configuration corresponding to an Envoy HTTP filter configuration. The config is responsible for
* creating a new HTTP filter that corresponds to each HTTP stream.
*
* This has 1:1 correspondence with the DynamicModuleHttpFilterConfig class in Envoy.
*
* OWNERSHIP: The module is responsible for managing the lifetime of the pointer. The pointer can be
* released when envoy_dynamic_module_on_http_filter_config_destroy is called for the same pointer.
*/
typedef const char* envoy_dynamic_module_type_abi_version; // NOLINT(modernize-use-using)
typedef const void* // NOLINT(modernize-use-using)
envoy_dynamic_module_type_http_filter_config_module_ptr;

// -----------------------------------------------------------------------------
// ------------------------------- Event Hooks ---------------------------------
Expand All @@ -54,10 +87,40 @@ typedef const char* envoy_dynamic_module_type_abi_version; // NOLINT(modernize-u
* to check compatibility and gracefully fail the initialization because there is no way to
* report an error to Envoy.
*
* @return envoy_dynamic_module_type_abi_version is the ABI version of the dynamic module. Null
* means the error and the module will be unloaded immediately.
* @return envoy_dynamic_module_type_abi_version_envoy_ptr is the ABI version of the dynamic
* module. Null means the error and the module will be unloaded immediately.
*/
envoy_dynamic_module_type_abi_version_envoy_ptr envoy_dynamic_module_on_program_init();

/**
* envoy_dynamic_module_on_http_filter_config_new is called by the main thread when the http
* filter config is loaded. The function returns a
* envoy_dynamic_module_type_http_filter_config_module_ptr for given name and config.
*
* @param filter_config_envoy_ptr is the pointer to the DynamicModuleHttpFilterConfig object for the
* corresponding config.
* @param name_ptr is the name of the filter.
* @param name_size is the size of the name.
* @param config_ptr is the configuration for the module.
* @param config_size is the size of the configuration.
* @return envoy_dynamic_module_type_http_filter_config_module_ptr is the pointer to the
* in-module HTTP filter configuration. Returning nullptr indicates a failure to initialize the
* module. When it fails, the filter configuration will be rejected.
*/
envoy_dynamic_module_type_http_filter_config_module_ptr
envoy_dynamic_module_on_http_filter_config_new(
envoy_dynamic_module_type_http_filter_config_envoy_ptr filter_config_envoy_ptr,
const char* name_ptr, int name_size, const char* config_ptr, int config_size);

/**
* envoy_dynamic_module_on_http_filter_config_destroy is called when the HTTP filter configuration
* is destroyed in Envoy. The module should release any resources associated with the corresponding
* in-module HTTP filter configuration.
* @param filter_config_ptr is a pointer to the in-module HTTP filter configuration whose
* corresponding Envoy HTTP filter configuration is being destroyed.
*/
envoy_dynamic_module_type_abi_version envoy_dynamic_module_on_program_init();
void envoy_dynamic_module_on_http_filter_config_destroy(
envoy_dynamic_module_type_http_filter_config_module_ptr filter_config_ptr);

#ifdef __cplusplus
}
Expand Down
2 changes: 1 addition & 1 deletion source/extensions/dynamic_modules/abi_version.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace DynamicModules {
#endif
// This is the ABI version calculated as a sha256 hash of the ABI header files. When the ABI
// changes, this value must change, and the correctness of this value is checked by the test.
const char* kAbiVersion = "4293760426255b24c25b97a18d9fd31b4d1956f10ba0ff2f723580a46ee8fa21";
const char* kAbiVersion = "164a60ff214ca3cd62526ddb7c3fe21cf943e8721a115c87feca81a58510072c";

#ifdef __cplusplus
} // namespace DynamicModules
Expand Down
12 changes: 6 additions & 6 deletions source/extensions/dynamic_modules/dynamic_modules.cc
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ namespace DynamicModules {

constexpr char DYNAMIC_MODULES_SEARCH_PATH[] = "ENVOY_DYNAMIC_MODULES_SEARCH_PATH";

absl::StatusOr<DynamicModuleSharedPtr> newDynamicModule(const absl::string_view object_file_path,
const bool do_not_close) {
absl::StatusOr<DynamicModulePtr> 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.
Expand All @@ -33,15 +33,15 @@ absl::StatusOr<DynamicModuleSharedPtr> newDynamicModule(const absl::string_view
absl::StrCat("Failed to load dynamic module: ", object_file_path, " : ", dlerror()));
}

DynamicModuleSharedPtr dynamic_module = std::make_shared<DynamicModule>(handle);
DynamicModulePtr dynamic_module = std::make_unique<DynamicModule>(handle);

const auto init_function =
dynamic_module->getFunctionPointer<decltype(&envoy_dynamic_module_on_program_init)>(
"envoy_dynamic_module_on_program_init");

if (init_function == nullptr) {
return absl::InvalidArgumentError(
absl::StrCat("Failed to resolve envoy_dynamic_module_on_program_init: ", dlerror()));
"Failed to resolve symbol envoy_dynamic_module_on_program_init");
}

const char* abi_version = (*init_function)();
Expand All @@ -57,8 +57,8 @@ absl::StatusOr<DynamicModuleSharedPtr> newDynamicModule(const absl::string_view
return dynamic_module;
}

absl::StatusOr<DynamicModuleSharedPtr> newDynamicModuleByName(const absl::string_view module_name,
const bool do_not_close) {
absl::StatusOr<DynamicModulePtr> newDynamicModuleByName(const absl::string_view module_name,
const bool do_not_close) {
const char* module_search_path = getenv(DYNAMIC_MODULES_SEARCH_PATH);
if (module_search_path == nullptr) {
return absl::InvalidArgumentError(absl::StrCat("Failed to load dynamic module: ", module_name,
Expand Down
10 changes: 5 additions & 5 deletions source/extensions/dynamic_modules/dynamic_modules.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class DynamicModule {
void* handle_;
};

using DynamicModuleSharedPtr = std::shared_ptr<DynamicModule>;
using DynamicModulePtr = std::unique_ptr<DynamicModule>;

/**
* Creates a new DynamicModule. This is mainly exposed for testing purposes. Use
Expand All @@ -58,8 +58,8 @@ using DynamicModuleSharedPtr = std::shared_ptr<DynamicModule>;
* 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);
absl::StatusOr<DynamicModulePtr> newDynamicModule(const absl::string_view object_file_path,
const bool do_not_close);

/**
* Creates a new DynamicModule by name under the search path specified by the environment variable
Expand All @@ -70,8 +70,8 @@ absl::StatusOr<DynamicModuleSharedPtr> newDynamicModule(const absl::string_view
* will not be destroyed. This is useful when an object has some global state that should not be
* terminated.
*/
absl::StatusOr<DynamicModuleSharedPtr> newDynamicModuleByName(const absl::string_view module_name,
const bool do_not_close);
absl::StatusOr<DynamicModulePtr> newDynamicModuleByName(const absl::string_view module_name,
const bool do_not_close);

} // namespace DynamicModules
} // namespace Extensions
Expand Down
141 changes: 133 additions & 8 deletions source/extensions/dynamic_modules/sdk/rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,45 @@ pub mod abi {
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
}

/// Declare the init function for the dynamic module. This function is called when the dynamic module is loaded.
/// The function must return true on success, and false on failure. When it returns false,
/// the dynamic module will not be loaded.
/// Declare the init functions for the dynamic module.
///
/// This is useful to perform any process-wide initialization that the dynamic module needs.
/// The first argument has [`ProgramInitFunction`] type, and it is called when the dynamic module is loaded.
///
/// The second argument has [`NewHttpFilterConfigFunction`] type, and it is called when the new HTTP filter configuration is created.
Copy link
Member

Choose a reason for hiding this comment

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

nit: can you setup in some future PR rustfmt and make sure it runs in CI? This config https://github.com/bitdriftlabs/shared-core/blob/main/rustfmt.toml will make it mostly look like Envoy code style. For now you can run it manually.

Copy link
Member Author

Choose a reason for hiding this comment

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

sure, it's already enabled in this PR with the default config

# As per the discussion in https://github.com/envoyproxy/envoy/pull/35627,
# we set the rust_fmt and clippy target here instead of the part of //tools/code_format target for now.
rustfmt_test(
name = "rust_sdk_fmt",
tags = ["nocoverage"],
targets = ["//source/extensions/dynamic_modules/sdk/rust:envoy_proxy_dynamic_modules_rust_sdk"],
)
rust_clippy(
name = "rust_sdk_clippy",
tags = ["nocoverage"],
deps = ["//source/extensions/dynamic_modules/sdk/rust:envoy_proxy_dynamic_modules_rust_sdk"],
)

so i will follow up with the TOML config after this

///
/// # Example
///
/// ```
/// use envoy_proxy_dynamic_modules_rust_sdk::declare_program_init;
/// use envoy_proxy_dynamic_modules_rust_sdk::*;
///
/// declare_program_init!(my_program_init);
/// declare_init_functions!(my_program_init, my_new_http_filter_config_fn);
///
/// fn my_program_init() -> bool {
/// true
/// }
///
/// fn my_new_http_filter_config_fn(
/// _envoy_filter_config: EnvoyHttpFilterConfig,
/// _name: &str,
/// _config: &str,
/// ) -> Option<Box<dyn HttpFilterConfig>> {
/// Some(Box::new(MyHttpFilterConfig {}))
/// }
///
/// struct MyHttpFilterConfig {}
///
/// impl HttpFilterConfig for MyHttpFilterConfig {}
/// ```
#[macro_export]
macro_rules! declare_program_init {
($f:ident) => {
macro_rules! declare_init_functions {
($f:ident,$new_http_filter_config_fn:expr) => {
#[no_mangle]
pub extern "C" fn envoy_dynamic_module_on_program_init() -> *const ::std::os::raw::c_char {
unsafe {
// We can assume that this is only called once at the beginning of the program, so it is safe to set a mutable global variable.
envoy_proxy_dynamic_modules_rust_sdk::NEW_HTTP_FILTER_CONFIG_FUNCTION =
$new_http_filter_config_fn
};
mathetake marked this conversation as resolved.
Show resolved Hide resolved
if ($f()) {
envoy_proxy_dynamic_modules_rust_sdk::abi::kAbiVersion.as_ptr()
as *const ::std::os::raw::c_char
Expand All @@ -41,3 +58,111 @@ macro_rules! declare_program_init {
}
};
}

/// The function signature for the program init function.
///
/// This is called when the dynamic module is loaded, and it must return true on success, and false on failure. When it returns false,
/// the dynamic module will not be loaded.
///
/// This is useful to perform any process-wide initialization that the dynamic module needs.
pub type ProgramInitFunction = fn() -> bool;

/// The function signature for the new HTTP filter configuration function.
///
/// This is called when a new HTTP filter configuration is created, and it must return a new instance of the [`HttpFilterConfig`] object.
/// Returning `None` will cause the HTTP filter configuration to be rejected.
//
// TODO(@mathetake): I guess there would be a way to avoid the use of dyn in the first place.
// E.g. one idea is to accept all concrete type parameters for HttpFilterConfig and HttpFilter traits in declare_init_functions!,
// and generate the match statement based on that.
pub type NewHttpFilterConfigFunction = fn(
envoy_filter_config: EnvoyHttpFilterConfig,
name: &str,
config: &str,
) -> Option<Box<dyn HttpFilterConfig>>;

/// The global init function for HTTP filter configurations. This is set via the `declare_init_functions` macro,
/// and is not intended to be set directly.
pub static mut NEW_HTTP_FILTER_CONFIG_FUNCTION: NewHttpFilterConfigFunction = |_, _, _| {
panic!("NEW_HTTP_FILTER_CONFIG_FUNCTION is not set");
};
mathetake marked this conversation as resolved.
Show resolved Hide resolved

/// The trait that represents the configuration for an Envoy Http filter configuration.
/// This has one to one mapping with the [`EnvoyHttpFilterConfig`] object.
///
/// The object is created when the corresponding Envoy Http filter config is created, and it is
/// dropped when the corresponding Envoy Http filter config is destroyed. Therefore, the imlementation
/// is recommended to implement the [`Drop`] trait to handle the necessary cleanup.
pub trait HttpFilterConfig {
/// This is called when a HTTP filter chain is created for a new stream.
fn new_http_filter(&self) -> Box<dyn HttpFilter> {
unimplemented!() // TODO.
}
}

/// The trait that represents an Envoy Http filter for each stream.
pub trait HttpFilter {} // TODO.

/// An opaque object that represents the underlying Envoy Http filter config. This has one to one
/// mapping with the Envoy Http filter config object as well as [`HttpFilterConfig`] object.
///
/// This is a shallow wrapper around the raw pointer to the Envoy HTTP filter config object, and it
/// can be copied and used up until the corresponding [`HttpFilterConfig`] is dropped.
//
// TODO(@mathetake): make this only avaialble for non-test code, and provide a mock for testing. So that users
// can write a unit tests for their HttpFilterConfig implementations.
#[derive(Debug, Clone, Copy)]
pub struct EnvoyHttpFilterConfig {
raw_ptr: abi::envoy_dynamic_module_type_http_filter_config_envoy_ptr,
}

#[no_mangle]
unsafe extern "C" fn envoy_dynamic_module_on_http_filter_config_new(
envoy_filter_config_ptr: abi::envoy_dynamic_module_type_http_filter_config_envoy_ptr,
name_ptr: *const u8,
name_size: usize,
config_ptr: *const u8,
config_size: usize,
) -> abi::envoy_dynamic_module_type_http_filter_config_module_ptr {
mathetake marked this conversation as resolved.
Show resolved Hide resolved
// This assumes that the name and config are valid UTF-8 strings. Should we relax? At the moment, both are String at protobuf level.
let name = if name_size > 0 {
let slice = std::slice::from_raw_parts(name_ptr, name_size);
std::str::from_utf8(slice).unwrap()
} else {
""
};
let config = if config_size > 0 {
let slice = std::slice::from_raw_parts(config_ptr, config_size);
std::str::from_utf8(slice).unwrap()
} else {
""
};

let envoy_filter_config = EnvoyHttpFilterConfig {
raw_ptr: envoy_filter_config_ptr,
};

let filter_config =
if let Some(config) = NEW_HTTP_FILTER_CONFIG_FUNCTION(envoy_filter_config, name, config) {
config
} else {
return std::ptr::null();
};

// We wrap the Box<dyn HttpFilterConfig> in another Box to ensuure that the object is not dropped after being into a raw pointer.
// To be honest, this seems like a hack, and we should find a better way to handle this.
// See https://users.rust-lang.org/t/sending-a-boxed-trait-over-ffi/21708 for the exact problem.
let boxed_filter_config_ptr = Box::into_raw(Box::new(filter_config));
boxed_filter_config_ptr as abi::envoy_dynamic_module_type_http_filter_config_module_ptr
}

#[no_mangle]
unsafe extern "C" fn envoy_dynamic_module_on_http_filter_config_destroy(
http_filter: abi::envoy_dynamic_module_type_http_filter_config_module_ptr,
) {
let config = http_filter as *mut *mut dyn HttpFilterConfig;

// Drop the Box<dyn HttpFilterConfig> and the Box<*mut dyn HttpFilterConfig>
let _outer = Box::from_raw(config);
let _inner = Box::from_raw(*config);
}
20 changes: 14 additions & 6 deletions source/extensions/filters/http/dynamic_modules/factory.cc
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,28 @@ absl::StatusOr<Http::FilterFactoryCb> DynamicModuleConfigFactory::createFilterFa
raw_config, context.messageValidationVisitor());

const auto& module_config = proto_config.dynamic_module_config();
const auto dynamic_module = Extensions::DynamicModules::newDynamicModuleByName(
auto dynamic_module = Extensions::DynamicModules::newDynamicModuleByName(
module_config.name(), module_config.do_not_close());
if (!dynamic_module.ok()) {
return absl::InvalidArgumentError("Failed to load dynamic module: " +
std::string(dynamic_module.status().message()));
}
auto filter_config = std::make_shared<
Envoy::Extensions::DynamicModules::HttpFilters::DynamicModuleHttpFilterConfig>(
proto_config.filter_name(), proto_config.filter_config(), dynamic_module.value());

return [filter_config](Http::FilterChainFactoryCallbacks& callbacks) -> void {
absl::StatusOr<
Envoy::Extensions::DynamicModules::HttpFilters::DynamicModuleHttpFilterConfigSharedPtr>
filter_config =
Envoy::Extensions::DynamicModules::HttpFilters::newDynamicModuleHttpFilterConfig(
proto_config.filter_name(), proto_config.filter_config(),
std::move(dynamic_module.value()));

if (!filter_config.ok()) {
return absl::InvalidArgumentError("Failed to create filter config: " +
std::string(filter_config.status().message()));
}
return [config = filter_config.value()](Http::FilterChainFactoryCallbacks& callbacks) -> void {
auto filter =
std::make_shared<Envoy::Extensions::DynamicModules::HttpFilters::DynamicModuleHttpFilter>(
filter_config);
config);
callbacks.addStreamDecoderFilter(filter);
callbacks.addStreamEncoderFilter(filter);
};
Expand Down
Loading