From 214defb3298e13afae7f65d8f16756b00620d418 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Thu, 11 Jun 2026 19:12:40 +0200 Subject: [PATCH 01/14] feat: add initial version of the tracing extension --- sentry-tracing.c | 617 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 617 insertions(+) create mode 100644 sentry-tracing.c diff --git a/sentry-tracing.c b/sentry-tracing.c new file mode 100644 index 0000000..40811c8 --- /dev/null +++ b/sentry-tracing.c @@ -0,0 +1,617 @@ +#include "php.h" +#include "Zend/zend_observer.h" +#include "Zend/zend_exceptions.h" +#include "Zend/zend_attributes.h" +#include + +/** + * Stores the functions that should be observed. If a function is not in this map, + * it will not be measured. + */ +static HashTable sentry_tracing_hook_keys; + +/** + * Stores the call state of currently active functions. Mainly used to store start time + * to be able to calculate duration after invocation ends. + */ +static HashTable sentry_tracing_active_calls; + +/** + * This callback is invoked before a function is observed. The return value is stored and + * will be passed into the end callback, so specific data can be stored between the callbacks. + */ +static zval sentry_tracing_start_callback; + +/** + * This callback is invoked whenever the observer finishes. It will be invoked with + * the call state so that the PHP part can create new Spans etc. + */ +static zval sentry_tracing_end_callback; + +static zend_class_entry *sentry_instrumented_attribute_ce; + +/** + * Holds the name of the \Sentry\Instrumented attribute that can be used to retrieve it + * using zend_get_attribute. + */ +static zend_string *sentry_instrumented_attribute_lcname; + +// ==== CALL STATE BEGIN ==== + +/** + * Tracks the state per function/method call. We need this to calculate how long an invocation took + * and to produce proper spans + */ +typedef struct { + zend_string *name; + double start_time; + zval user_state; + zval metadata; +} sentry_tracing_call_state; + +/** + * Destructor for the call state struct + */ +static void sentry_tracing_call_state_dtor(zval *zv) { + sentry_tracing_call_state *state = Z_PTR_P(zv); + + if (state->name != NULL) { + zend_string_release(state->name); + } + + zval_ptr_dtor(&state->user_state); + zval_ptr_dtor(&state->metadata); + + efree(state); +} + +// ==== CALL STATE END ==== + +// ==== ATTRIBUTE START ==== +static zend_attribute *sentry_tracing_get_instrumented_attribute(zend_execute_data *execute_data) { + const zend_function *func = execute_data->func; + + if (func->common.attributes == NULL) { + return NULL; + } + + zend_attribute *attribute = zend_get_attribute( + func->common.attributes, + sentry_instrumented_attribute_lcname + ); + + return attribute; +} + +static bool sentry_tracing_has_instrumented_attribute(zend_execute_data *execute_data) { + return sentry_tracing_get_instrumented_attribute(execute_data) != NULL; +} + +static void sentry_tracing_get_attribute_metadata( + zend_execute_data *execute_data, + zval *metadata +) { + array_init(metadata); + + zend_attribute *attribute = sentry_tracing_get_instrumented_attribute(execute_data); + if (attribute != NULL && attribute->argc > 0) { + zval attribute_arg; + ZVAL_UNDEF(&attribute_arg); + + if (zend_get_attribute_value( + &attribute_arg, + attribute, + 0, + execute_data->func->common.scope + ) == SUCCESS && Z_TYPE(attribute_arg) == IS_ARRAY) { + zend_hash_copy( + Z_ARRVAL_P(metadata), + Z_ARRVAL(attribute_arg), + zval_add_ref + ); + } + + if (!Z_ISUNDEF(attribute_arg)) { + zval_ptr_dtor(&attribute_arg); + } + } +} + + +PHP_METHOD(Sentry_Instrumented, __construct) { + zval *metadata = NULL; + + ZEND_PARSE_PARAMETERS_START(0, 1) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY(metadata) + ZEND_PARSE_PARAMETERS_END(); +} + +ZEND_BEGIN_ARG_INFO_EX(arginfo_sentry_instrumented_attribute_construct, 0, 0, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, metadata, IS_ARRAY, 0, "[]") +ZEND_END_ARG_INFO(); + +static const zend_function_entry sentry_instrumented_attribute_methods[] = { + ZEND_ME(Sentry_Instrumented, __construct, arginfo_sentry_instrumented_attribute_construct, ZEND_ACC_PUBLIC) + PHP_FE_END +}; + +// ==== ATTRIBUTE END ====== + +/** + * Returns the current timestamp as float. Equivalent to microtime(true) in PHP. + */ +static double current_timestamp_as_float() { + return zend_hrtime() / 1000000000.0; +} + +static zend_result call_user_function_ignore(zval *callback, zval *retval, uint32_t param_count, zval *params) { + zend_result result = call_user_function( + EG(function_table), + NULL, + callback, + retval, + param_count, + params + ); + + if (EG(exception) != NULL) { + zend_clear_exception(); + } + return result; +} + +static zend_string *sentry_tracing_build_display_name( + zend_execute_data *execute_data +) { + const zend_function *func = execute_data->func; + + if (func->common.function_name == NULL) { + return NULL; + } + + if (func->common.scope != NULL) { + return zend_strpprintf( + 0, + "%s::%s", + ZSTR_VAL(func->common.scope->name), + ZSTR_VAL(func->common.function_name) + ); + } + + return zend_string_copy(func->common.function_name); +} + +PHP_FUNCTION(sentry_tracing_hook) { + zend_string *class_name = NULL; + zend_string *function_name; + zend_string *key; + zval metadata; + zval *extra_metadata = NULL; + + ZEND_PARSE_PARAMETERS_START(2,3) + Z_PARAM_STR_OR_NULL(class_name) + Z_PARAM_STR(function_name) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY(extra_metadata) + ZEND_PARSE_PARAMETERS_END(); + + array_init(&metadata); + + if (extra_metadata != NULL) { + zend_hash_copy( + Z_ARRVAL(metadata), + Z_ARRVAL_P(extra_metadata), + zval_add_ref + ); + } + + if (class_name != NULL) { + key = zend_strpprintf( + 0, + "%s::%s", + ZSTR_VAL(class_name), + ZSTR_VAL(function_name) + ); + } else { + key = zend_string_copy(function_name); + } + + zend_string *lowercase_key = zend_string_tolower(key); + zend_string_release(key); + + const zval* inserted = zend_hash_add(&sentry_tracing_hook_keys, lowercase_key, &metadata); + + // If the element wasn't inserted we have to manually destroy the local value to prevent memory leaks + if (inserted == NULL) { + zval_ptr_dtor(&metadata); + } + + zend_string_release(lowercase_key); + + RETURN_BOOL(inserted != NULL); +} + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX( + arginfo_sentry_tracing_hook_key, + 0, + 2, + _IS_BOOL, + 0 +) + ZEND_ARG_TYPE_INFO(0, class_name, IS_STRING, 1) + ZEND_ARG_TYPE_INFO(0, function_name, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, extra_metadata, IS_ARRAY, 0, "[]") +ZEND_END_ARG_INFO(); + + + + +PHP_FUNCTION(sentry_tracing_set_end_callback) { + zval *callback; + + ZEND_PARSE_PARAMETERS_START(1,1) + Z_PARAM_ZVAL(callback) + ZEND_PARSE_PARAMETERS_END(); + + if (!zend_is_callable(callback, 0, NULL)) { + zend_argument_type_error(1, "must be a valid callback"); + RETURN_THROWS(); + } + + if (!Z_ISUNDEF(sentry_tracing_end_callback)) { + zval_ptr_dtor(&sentry_tracing_end_callback); + } + + ZVAL_COPY(&sentry_tracing_end_callback, callback); + + RETURN_TRUE; +} + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX( + arginfo_sentry_tracing_set_end_callback, + 0, + 1, + _IS_BOOL, + 0 +) + ZEND_ARG_TYPE_INFO(0, callback, IS_CALLABLE, 0) +ZEND_END_ARG_INFO(); + + +PHP_FUNCTION(sentry_tracing_set_start_callback) { + zval *callback; + + ZEND_PARSE_PARAMETERS_START(1,1) + Z_PARAM_ZVAL(callback) + ZEND_PARSE_PARAMETERS_END(); + + if (!zend_is_callable(callback, 0, NULL)) { + zend_argument_type_error(1, "must be a valid callback"); + RETURN_THROWS(); + } + + if (!Z_ISUNDEF(sentry_tracing_start_callback)) { + zval_ptr_dtor(&sentry_tracing_start_callback); + } + + ZVAL_COPY(&sentry_tracing_start_callback, callback); + + RETURN_TRUE; +} + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX( + arginfo_sentry_tracing_set_start_callback, + 0, + 1, + _IS_BOOL, + 0 +) + ZEND_ARG_TYPE_INFO(0, callback, IS_CALLABLE, 0) +ZEND_END_ARG_INFO(); + +/** + * Returns a normalized function key that can be used as unique identifier to register hooks for it. + * The key will always be in lowercase to support the case insensitivity of PHP functions. + */ +static zend_string *sentry_tracing_build_function_key(zend_execute_data *execute_data) { + const zend_function *func = execute_data->func; + zend_string *key; + + if (func->common.function_name == NULL) { + return NULL; + } + + if (func->common.scope != NULL) { + key = strpprintf( + 0, + "%s::%s", + ZSTR_VAL(func->common.scope->name), + ZSTR_VAL(func->common.function_name) + ); + } else { + key = zend_string_copy(func->common.function_name); + } + + zend_string *lowercase_key = zend_string_tolower(key); + + zend_string_release(key); + + return lowercase_key; +} + +static bool sentry_tracing_should_observe(zend_execute_data *execute_data) { + zend_string *key = sentry_tracing_build_function_key(execute_data); + + if (key == NULL) { + return false; + } + + const bool explicitly_hooked = zend_hash_exists(&sentry_tracing_hook_keys, key); + + zend_string_release(key); + + if (explicitly_hooked) { + return true; + } + + return sentry_tracing_has_instrumented_attribute(execute_data); +} + +static void sentry_tracing_observer_begin(zend_execute_data *execute_data) { + zend_string *key = sentry_tracing_build_function_key(execute_data); + if (key == NULL) { + return; + } + + zval default_metadata; + bool using_default_metadata = false; + + zval *metadata = zend_hash_find(&sentry_tracing_hook_keys, key); + + if (metadata == NULL) { + sentry_tracing_get_attribute_metadata(execute_data, &default_metadata); + metadata = &default_metadata; + using_default_metadata = true; + } + + if (Z_TYPE_P(metadata) == IS_ARRAY) { + zend_string *name = sentry_tracing_build_display_name(execute_data); + if (name == NULL) { + zend_string_release(key); + return; + } + + zval state_zv; + zval retval; + ZVAL_UNDEF(&retval); + + if (!Z_ISUNDEF(sentry_tracing_start_callback)) { + zval params[1]; + + ZVAL_COPY(¶ms[0], metadata); + + zend_result success = call_user_function_ignore( + &sentry_tracing_start_callback, + &retval, + 1, + params + ); + + zval_ptr_dtor(¶ms[0]); + + if (success != SUCCESS || Z_ISUNDEF(retval)) { + ZVAL_NULL(&retval); + } + } + + sentry_tracing_call_state *state = emalloc(sizeof(sentry_tracing_call_state)); + state->name = name; + state->start_time = current_timestamp_as_float(); + + ZVAL_COPY(&state->metadata, metadata); + + if (Z_ISUNDEF(retval)) { + ZVAL_NULL(&retval); + } + state->user_state = retval; + + + ZVAL_PTR(&state_zv, state); + + + zend_hash_index_update( + &sentry_tracing_active_calls, + (zend_ulong) (uintptr_t) execute_data, + &state_zv + ); + } + + if (using_default_metadata) { + zval_ptr_dtor(&default_metadata); + } + + zend_string_release(key); +} + + +static void sentry_tracing_observer_end(zend_execute_data *execute_data, zval *return_value) { + zend_ulong hash_key = (zend_ulong) (uintptr_t) execute_data; + + if (EG(exception) != NULL) { + zend_hash_index_del( + &sentry_tracing_active_calls, + hash_key + ); + return; + } + + zval *state_zv = zend_hash_index_find(&sentry_tracing_active_calls, hash_key); + + if (state_zv == NULL || Z_TYPE_P(state_zv) != IS_PTR) { + return; + } + + sentry_tracing_call_state *state = Z_PTR_P(state_zv); + + double end_time = current_timestamp_as_float(); + double duration = end_time - state->start_time; + + if (!Z_ISUNDEF(sentry_tracing_end_callback)) { + zval event; + zval retval; + ZVAL_UNDEF(&retval); + zval params[2]; + + array_init(&event); + + if (state->name != NULL) { + add_assoc_str(&event, "name", zend_string_copy(state->name)); + } + + add_assoc_double(&event, "start_time", state->start_time); + add_assoc_double(&event, "end_time", end_time); + add_assoc_double(&event, "duration", duration); + + zval metadata_zv; + ZVAL_COPY(&metadata_zv, &state->metadata); + add_assoc_zval(&event, "metadata", &metadata_zv); + + ZVAL_COPY_VALUE(¶ms[0], &event); + + ZVAL_COPY(¶ms[1], &state->user_state); + + call_user_function_ignore( + &sentry_tracing_end_callback, + &retval, + 2, + params + ); + + if (!Z_ISUNDEF(retval)) { + zval_ptr_dtor(&retval); + } + + zval_ptr_dtor(¶ms[1]); + zval_ptr_dtor(&event); + } + + zend_hash_index_del( + &sentry_tracing_active_calls, + hash_key + ); +} + +static zend_observer_fcall_handlers sentry_tracing_observer(zend_execute_data *execute_data) { + zend_observer_fcall_handlers handlers = {0}; + + if (sentry_tracing_should_observe(execute_data)) { + handlers.begin = sentry_tracing_observer_begin; + handlers.end = sentry_tracing_observer_end; + } + + return handlers; +} + +PHP_MINIT_FUNCTION(sentry_tracing) { + zend_class_entry ce; + + INIT_NS_CLASS_ENTRY( + ce, + "Sentry", + "Instrumented", + sentry_instrumented_attribute_methods + ); + + sentry_instrumented_attribute_ce = zend_register_internal_class(&ce); + sentry_instrumented_attribute_ce->ce_flags |= ZEND_ACC_FINAL; + + zend_string *attribute_name = zend_string_init_interned( + "Attribute", + sizeof("Attribute")-1, + 1 + ); + + zend_attribute *attribute = zend_add_class_attribute( + sentry_instrumented_attribute_ce, + attribute_name, + 1 + ); + + zend_string_release(attribute_name); + + ZVAL_LONG( + &attribute->args[0].value, + ZEND_ATTRIBUTE_TARGET_FUNCTION | ZEND_ATTRIBUTE_TARGET_METHOD + ); + + sentry_instrumented_attribute_lcname = zend_string_init_interned( + "sentry\\instrumented", + sizeof("sentry\\instrumented") - 1, + 1 + ); + + zend_observer_fcall_register(sentry_tracing_observer); + + return SUCCESS; +} + +PHP_RINIT_FUNCTION(sentry_tracing) { + zend_hash_init(&sentry_tracing_hook_keys, 8, NULL, ZVAL_PTR_DTOR, 0); + zend_hash_init(&sentry_tracing_active_calls, 8, NULL, sentry_tracing_call_state_dtor, 0); + + ZVAL_UNDEF(&sentry_tracing_start_callback); + ZVAL_UNDEF(&sentry_tracing_end_callback); + + return SUCCESS; +} + +PHP_RSHUTDOWN_FUNCTION(sentry_tracing) { + zend_hash_destroy(&sentry_tracing_hook_keys); + zend_hash_destroy(&sentry_tracing_active_calls); + + if (!Z_ISUNDEF(sentry_tracing_start_callback)) { + zval_ptr_dtor(&sentry_tracing_start_callback); + ZVAL_UNDEF(&sentry_tracing_start_callback); + } + + if (!Z_ISUNDEF(sentry_tracing_end_callback)) { + zval_ptr_dtor(&sentry_tracing_end_callback); + ZVAL_UNDEF(&sentry_tracing_end_callback); + } + + return SUCCESS; +} + +PHP_MSHUTDOWN_FUNCTION(sentry_tracing) { + if (sentry_instrumented_attribute_lcname != NULL) { + zend_string_release(sentry_instrumented_attribute_lcname); + sentry_instrumented_attribute_lcname = NULL; + } + + return SUCCESS; +} + + +static const zend_function_entry sentry_tracing_functions[] = { + ZEND_NS_FENTRY("Sentry\\Instrumentation", hook, ZEND_FN(sentry_tracing_hook), arginfo_sentry_tracing_hook_key, 0) + ZEND_NS_FENTRY("Sentry\\Instrumentation", setEndCallback, ZEND_FN(sentry_tracing_set_end_callback), arginfo_sentry_tracing_set_end_callback, 0) + ZEND_NS_FENTRY("Sentry\\Instrumentation", setStartCallback, ZEND_FN(sentry_tracing_set_start_callback), arginfo_sentry_tracing_set_start_callback, 0) + PHP_FE_END +}; + +zend_module_entry sentry_module_entry = { + STANDARD_MODULE_HEADER, + "sentry", + sentry_tracing_functions, + PHP_MINIT(sentry_tracing), + PHP_MSHUTDOWN(sentry_tracing), + PHP_RINIT(sentry_tracing), + PHP_RSHUTDOWN(sentry_tracing), + NULL, + "0.1.0", + STANDARD_MODULE_PROPERTIES +}; + +ZEND_GET_MODULE(sentry); + From a1b6e0c5c396c4067f2bdb1414867c7f7effc01f Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Sun, 14 Jun 2026 14:53:40 +0200 Subject: [PATCH 02/14] feat(tracer): add initial version of the tracing extension --- .github/workflows/tests.yml | 58 ++++ README.md | 53 +++ config.w32 | 6 + php_sentry.h | 17 - sentry-tracing.c | 617 ---------------------------------- sentry.c | 650 +++++++++++++++++++++++++++++++++--- sentry.stub.php | 20 +- sentry_tracer.h | 10 - 8 files changed, 739 insertions(+), 692 deletions(-) create mode 100644 .github/workflows/tests.yml create mode 100644 config.w32 delete mode 100644 php_sentry.h delete mode 100644 sentry-tracing.c delete mode 100644 sentry_tracer.h diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..9c2886c --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,58 @@ +name: Tests + +on: + push: + branches: [main] + pull_request: + +jobs: + linux: + name: PHP ${{ matrix.php }} (Linux) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['8.0', '8.1', '8.2', '8.3', '8.4', '8.5'] + steps: + - uses: actions/checkout@v4 + + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + + - name: Build + run: | + phpize + ./configure --enable-sentry + make + + - name: Test + run: make test TESTS="--show-diff" NO_INTERACTION=1 + + windows: + name: PHP ${{ matrix.php-version }} (${{ matrix.arch }}, ${{ matrix.ts == '' && 'nts' || 'ts' }}) (Windows) + needs: get-windows-matrix + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.get-windows-matrix.outputs.matrix) }} + steps: + - uses: actions/checkout@v4 + + - uses: php/php-windows-builder/extension@v1 + with: + php-version: ${{ matrix.php-version }} + arch: ${{ matrix.arch }} + ts: ${{ matrix.ts }} + + get-windows-matrix: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.matrix.outputs.matrix }} + steps: + - uses: actions/checkout@v4 + + - id: matrix + uses: php/php-windows-builder/extension-matrix@v1 + with: + php-version-list: '8.0, 8.1, 8.2, 8.3, 8.4' diff --git a/README.md b/README.md index dfeea76..ebdf19d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,58 @@ # sentry-php-tracer +Sentry extension to enable automatic instrumentation of methods and functions. + +Methods/functions can be instrumented by either declaring the #[\Sentry\Trace] attribute on them +or registering them with `\Sentry\instrument("MyClass", "myFunction)`. + +The extension is meant to only provide telemetry information for methods/functions, such as `duration` or `start_time`. +Creating spans and maintaining a proper span will be done in the SDK to minimize the number of required updates +for the extension itself. + +## Callbacks + +The extension offers two callbacks, one before a function is executed and one after execution. + +Data returned in the startCallback will be passed back as second parameter of the endCallback. +This provides a way to pass data between the callbacks without a custom storage. + +Each callback gets a data array as first parameter with the following keys: + +| Key | When | Description | +|------------|-----------|--------------------------------------------------| +| start_time | start,end | The start time as float timestamp | +| end_time | end | The end time as float timestamp | +| duration | end | The duration in float milliseconds | +| name | start,end | The name of the function/method | +| metadata | start,end | The array with the metadata specified, see below | + +### Example + +```php +\Sentry\setStartCallback(static function (array $data) { + return ['spanId' => generateSpanId()]; +}); + +\Sentry\setEndCallback(static function (array $data, $userData) { + setTelemetryData([ + 'name' => $data['name'], + 'duration' => $data['duration'], + 'spanId' => $userData['spanId']; + ]); +}); + +``` + +## Attributes/Metadata + +Metadata/attributes can be provided in the attribute and the instrument function. + +Attribute: +`#[\Sentry\Trace(['my-attribute' => 'test', 'other' => 'foo'])]` + +Function: +`\Sentry\instrument("MyClass", "myFunction", ['my-attribute' => 'test', 'other' => 'foo']` + This is work in progress. ## Build the extension diff --git a/config.w32 b/config.w32 new file mode 100644 index 0000000..3641ff0 --- /dev/null +++ b/config.w32 @@ -0,0 +1,6 @@ +ARG_ENABLE("sentry", "whether to enable Sentry support", + "Enable Sentry support"); +if (PHP_SENTRY == "yes") { + AC_DEFINE("HAVE_SENTRY", 1, "Whether you have Sentry"); + EXTENSION("Sentry", "sentry.c", true); +} diff --git a/php_sentry.h b/php_sentry.h deleted file mode 100644 index a7d933f..0000000 --- a/php_sentry.h +++ /dev/null @@ -1,17 +0,0 @@ -#ifndef PHP_SENTRY_H -#define PHP_SENTRY_H - -extern zend_module_entry sentry_module_entry; -#define phpext_sentry_ptr &sentry_module_entry - -#define PHP_SENTRY_VERSION "1.0" -#define PHP_SENTRY_EXTNAME "sentry" - -PHP_FUNCTION(sentry); -PHP_FUNCTION(Sentry_trace); - -# if defined(ZTS) && defined(COMPILE_DL_TEST) -ZEND_TSRMLS_CACHE_EXTERN() -# endif - -#endif /* PHP_SENTRY_H */ diff --git a/sentry-tracing.c b/sentry-tracing.c deleted file mode 100644 index 40811c8..0000000 --- a/sentry-tracing.c +++ /dev/null @@ -1,617 +0,0 @@ -#include "php.h" -#include "Zend/zend_observer.h" -#include "Zend/zend_exceptions.h" -#include "Zend/zend_attributes.h" -#include - -/** - * Stores the functions that should be observed. If a function is not in this map, - * it will not be measured. - */ -static HashTable sentry_tracing_hook_keys; - -/** - * Stores the call state of currently active functions. Mainly used to store start time - * to be able to calculate duration after invocation ends. - */ -static HashTable sentry_tracing_active_calls; - -/** - * This callback is invoked before a function is observed. The return value is stored and - * will be passed into the end callback, so specific data can be stored between the callbacks. - */ -static zval sentry_tracing_start_callback; - -/** - * This callback is invoked whenever the observer finishes. It will be invoked with - * the call state so that the PHP part can create new Spans etc. - */ -static zval sentry_tracing_end_callback; - -static zend_class_entry *sentry_instrumented_attribute_ce; - -/** - * Holds the name of the \Sentry\Instrumented attribute that can be used to retrieve it - * using zend_get_attribute. - */ -static zend_string *sentry_instrumented_attribute_lcname; - -// ==== CALL STATE BEGIN ==== - -/** - * Tracks the state per function/method call. We need this to calculate how long an invocation took - * and to produce proper spans - */ -typedef struct { - zend_string *name; - double start_time; - zval user_state; - zval metadata; -} sentry_tracing_call_state; - -/** - * Destructor for the call state struct - */ -static void sentry_tracing_call_state_dtor(zval *zv) { - sentry_tracing_call_state *state = Z_PTR_P(zv); - - if (state->name != NULL) { - zend_string_release(state->name); - } - - zval_ptr_dtor(&state->user_state); - zval_ptr_dtor(&state->metadata); - - efree(state); -} - -// ==== CALL STATE END ==== - -// ==== ATTRIBUTE START ==== -static zend_attribute *sentry_tracing_get_instrumented_attribute(zend_execute_data *execute_data) { - const zend_function *func = execute_data->func; - - if (func->common.attributes == NULL) { - return NULL; - } - - zend_attribute *attribute = zend_get_attribute( - func->common.attributes, - sentry_instrumented_attribute_lcname - ); - - return attribute; -} - -static bool sentry_tracing_has_instrumented_attribute(zend_execute_data *execute_data) { - return sentry_tracing_get_instrumented_attribute(execute_data) != NULL; -} - -static void sentry_tracing_get_attribute_metadata( - zend_execute_data *execute_data, - zval *metadata -) { - array_init(metadata); - - zend_attribute *attribute = sentry_tracing_get_instrumented_attribute(execute_data); - if (attribute != NULL && attribute->argc > 0) { - zval attribute_arg; - ZVAL_UNDEF(&attribute_arg); - - if (zend_get_attribute_value( - &attribute_arg, - attribute, - 0, - execute_data->func->common.scope - ) == SUCCESS && Z_TYPE(attribute_arg) == IS_ARRAY) { - zend_hash_copy( - Z_ARRVAL_P(metadata), - Z_ARRVAL(attribute_arg), - zval_add_ref - ); - } - - if (!Z_ISUNDEF(attribute_arg)) { - zval_ptr_dtor(&attribute_arg); - } - } -} - - -PHP_METHOD(Sentry_Instrumented, __construct) { - zval *metadata = NULL; - - ZEND_PARSE_PARAMETERS_START(0, 1) - Z_PARAM_OPTIONAL - Z_PARAM_ARRAY(metadata) - ZEND_PARSE_PARAMETERS_END(); -} - -ZEND_BEGIN_ARG_INFO_EX(arginfo_sentry_instrumented_attribute_construct, 0, 0, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, metadata, IS_ARRAY, 0, "[]") -ZEND_END_ARG_INFO(); - -static const zend_function_entry sentry_instrumented_attribute_methods[] = { - ZEND_ME(Sentry_Instrumented, __construct, arginfo_sentry_instrumented_attribute_construct, ZEND_ACC_PUBLIC) - PHP_FE_END -}; - -// ==== ATTRIBUTE END ====== - -/** - * Returns the current timestamp as float. Equivalent to microtime(true) in PHP. - */ -static double current_timestamp_as_float() { - return zend_hrtime() / 1000000000.0; -} - -static zend_result call_user_function_ignore(zval *callback, zval *retval, uint32_t param_count, zval *params) { - zend_result result = call_user_function( - EG(function_table), - NULL, - callback, - retval, - param_count, - params - ); - - if (EG(exception) != NULL) { - zend_clear_exception(); - } - return result; -} - -static zend_string *sentry_tracing_build_display_name( - zend_execute_data *execute_data -) { - const zend_function *func = execute_data->func; - - if (func->common.function_name == NULL) { - return NULL; - } - - if (func->common.scope != NULL) { - return zend_strpprintf( - 0, - "%s::%s", - ZSTR_VAL(func->common.scope->name), - ZSTR_VAL(func->common.function_name) - ); - } - - return zend_string_copy(func->common.function_name); -} - -PHP_FUNCTION(sentry_tracing_hook) { - zend_string *class_name = NULL; - zend_string *function_name; - zend_string *key; - zval metadata; - zval *extra_metadata = NULL; - - ZEND_PARSE_PARAMETERS_START(2,3) - Z_PARAM_STR_OR_NULL(class_name) - Z_PARAM_STR(function_name) - Z_PARAM_OPTIONAL - Z_PARAM_ARRAY(extra_metadata) - ZEND_PARSE_PARAMETERS_END(); - - array_init(&metadata); - - if (extra_metadata != NULL) { - zend_hash_copy( - Z_ARRVAL(metadata), - Z_ARRVAL_P(extra_metadata), - zval_add_ref - ); - } - - if (class_name != NULL) { - key = zend_strpprintf( - 0, - "%s::%s", - ZSTR_VAL(class_name), - ZSTR_VAL(function_name) - ); - } else { - key = zend_string_copy(function_name); - } - - zend_string *lowercase_key = zend_string_tolower(key); - zend_string_release(key); - - const zval* inserted = zend_hash_add(&sentry_tracing_hook_keys, lowercase_key, &metadata); - - // If the element wasn't inserted we have to manually destroy the local value to prevent memory leaks - if (inserted == NULL) { - zval_ptr_dtor(&metadata); - } - - zend_string_release(lowercase_key); - - RETURN_BOOL(inserted != NULL); -} - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX( - arginfo_sentry_tracing_hook_key, - 0, - 2, - _IS_BOOL, - 0 -) - ZEND_ARG_TYPE_INFO(0, class_name, IS_STRING, 1) - ZEND_ARG_TYPE_INFO(0, function_name, IS_STRING, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, extra_metadata, IS_ARRAY, 0, "[]") -ZEND_END_ARG_INFO(); - - - - -PHP_FUNCTION(sentry_tracing_set_end_callback) { - zval *callback; - - ZEND_PARSE_PARAMETERS_START(1,1) - Z_PARAM_ZVAL(callback) - ZEND_PARSE_PARAMETERS_END(); - - if (!zend_is_callable(callback, 0, NULL)) { - zend_argument_type_error(1, "must be a valid callback"); - RETURN_THROWS(); - } - - if (!Z_ISUNDEF(sentry_tracing_end_callback)) { - zval_ptr_dtor(&sentry_tracing_end_callback); - } - - ZVAL_COPY(&sentry_tracing_end_callback, callback); - - RETURN_TRUE; -} - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX( - arginfo_sentry_tracing_set_end_callback, - 0, - 1, - _IS_BOOL, - 0 -) - ZEND_ARG_TYPE_INFO(0, callback, IS_CALLABLE, 0) -ZEND_END_ARG_INFO(); - - -PHP_FUNCTION(sentry_tracing_set_start_callback) { - zval *callback; - - ZEND_PARSE_PARAMETERS_START(1,1) - Z_PARAM_ZVAL(callback) - ZEND_PARSE_PARAMETERS_END(); - - if (!zend_is_callable(callback, 0, NULL)) { - zend_argument_type_error(1, "must be a valid callback"); - RETURN_THROWS(); - } - - if (!Z_ISUNDEF(sentry_tracing_start_callback)) { - zval_ptr_dtor(&sentry_tracing_start_callback); - } - - ZVAL_COPY(&sentry_tracing_start_callback, callback); - - RETURN_TRUE; -} - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX( - arginfo_sentry_tracing_set_start_callback, - 0, - 1, - _IS_BOOL, - 0 -) - ZEND_ARG_TYPE_INFO(0, callback, IS_CALLABLE, 0) -ZEND_END_ARG_INFO(); - -/** - * Returns a normalized function key that can be used as unique identifier to register hooks for it. - * The key will always be in lowercase to support the case insensitivity of PHP functions. - */ -static zend_string *sentry_tracing_build_function_key(zend_execute_data *execute_data) { - const zend_function *func = execute_data->func; - zend_string *key; - - if (func->common.function_name == NULL) { - return NULL; - } - - if (func->common.scope != NULL) { - key = strpprintf( - 0, - "%s::%s", - ZSTR_VAL(func->common.scope->name), - ZSTR_VAL(func->common.function_name) - ); - } else { - key = zend_string_copy(func->common.function_name); - } - - zend_string *lowercase_key = zend_string_tolower(key); - - zend_string_release(key); - - return lowercase_key; -} - -static bool sentry_tracing_should_observe(zend_execute_data *execute_data) { - zend_string *key = sentry_tracing_build_function_key(execute_data); - - if (key == NULL) { - return false; - } - - const bool explicitly_hooked = zend_hash_exists(&sentry_tracing_hook_keys, key); - - zend_string_release(key); - - if (explicitly_hooked) { - return true; - } - - return sentry_tracing_has_instrumented_attribute(execute_data); -} - -static void sentry_tracing_observer_begin(zend_execute_data *execute_data) { - zend_string *key = sentry_tracing_build_function_key(execute_data); - if (key == NULL) { - return; - } - - zval default_metadata; - bool using_default_metadata = false; - - zval *metadata = zend_hash_find(&sentry_tracing_hook_keys, key); - - if (metadata == NULL) { - sentry_tracing_get_attribute_metadata(execute_data, &default_metadata); - metadata = &default_metadata; - using_default_metadata = true; - } - - if (Z_TYPE_P(metadata) == IS_ARRAY) { - zend_string *name = sentry_tracing_build_display_name(execute_data); - if (name == NULL) { - zend_string_release(key); - return; - } - - zval state_zv; - zval retval; - ZVAL_UNDEF(&retval); - - if (!Z_ISUNDEF(sentry_tracing_start_callback)) { - zval params[1]; - - ZVAL_COPY(¶ms[0], metadata); - - zend_result success = call_user_function_ignore( - &sentry_tracing_start_callback, - &retval, - 1, - params - ); - - zval_ptr_dtor(¶ms[0]); - - if (success != SUCCESS || Z_ISUNDEF(retval)) { - ZVAL_NULL(&retval); - } - } - - sentry_tracing_call_state *state = emalloc(sizeof(sentry_tracing_call_state)); - state->name = name; - state->start_time = current_timestamp_as_float(); - - ZVAL_COPY(&state->metadata, metadata); - - if (Z_ISUNDEF(retval)) { - ZVAL_NULL(&retval); - } - state->user_state = retval; - - - ZVAL_PTR(&state_zv, state); - - - zend_hash_index_update( - &sentry_tracing_active_calls, - (zend_ulong) (uintptr_t) execute_data, - &state_zv - ); - } - - if (using_default_metadata) { - zval_ptr_dtor(&default_metadata); - } - - zend_string_release(key); -} - - -static void sentry_tracing_observer_end(zend_execute_data *execute_data, zval *return_value) { - zend_ulong hash_key = (zend_ulong) (uintptr_t) execute_data; - - if (EG(exception) != NULL) { - zend_hash_index_del( - &sentry_tracing_active_calls, - hash_key - ); - return; - } - - zval *state_zv = zend_hash_index_find(&sentry_tracing_active_calls, hash_key); - - if (state_zv == NULL || Z_TYPE_P(state_zv) != IS_PTR) { - return; - } - - sentry_tracing_call_state *state = Z_PTR_P(state_zv); - - double end_time = current_timestamp_as_float(); - double duration = end_time - state->start_time; - - if (!Z_ISUNDEF(sentry_tracing_end_callback)) { - zval event; - zval retval; - ZVAL_UNDEF(&retval); - zval params[2]; - - array_init(&event); - - if (state->name != NULL) { - add_assoc_str(&event, "name", zend_string_copy(state->name)); - } - - add_assoc_double(&event, "start_time", state->start_time); - add_assoc_double(&event, "end_time", end_time); - add_assoc_double(&event, "duration", duration); - - zval metadata_zv; - ZVAL_COPY(&metadata_zv, &state->metadata); - add_assoc_zval(&event, "metadata", &metadata_zv); - - ZVAL_COPY_VALUE(¶ms[0], &event); - - ZVAL_COPY(¶ms[1], &state->user_state); - - call_user_function_ignore( - &sentry_tracing_end_callback, - &retval, - 2, - params - ); - - if (!Z_ISUNDEF(retval)) { - zval_ptr_dtor(&retval); - } - - zval_ptr_dtor(¶ms[1]); - zval_ptr_dtor(&event); - } - - zend_hash_index_del( - &sentry_tracing_active_calls, - hash_key - ); -} - -static zend_observer_fcall_handlers sentry_tracing_observer(zend_execute_data *execute_data) { - zend_observer_fcall_handlers handlers = {0}; - - if (sentry_tracing_should_observe(execute_data)) { - handlers.begin = sentry_tracing_observer_begin; - handlers.end = sentry_tracing_observer_end; - } - - return handlers; -} - -PHP_MINIT_FUNCTION(sentry_tracing) { - zend_class_entry ce; - - INIT_NS_CLASS_ENTRY( - ce, - "Sentry", - "Instrumented", - sentry_instrumented_attribute_methods - ); - - sentry_instrumented_attribute_ce = zend_register_internal_class(&ce); - sentry_instrumented_attribute_ce->ce_flags |= ZEND_ACC_FINAL; - - zend_string *attribute_name = zend_string_init_interned( - "Attribute", - sizeof("Attribute")-1, - 1 - ); - - zend_attribute *attribute = zend_add_class_attribute( - sentry_instrumented_attribute_ce, - attribute_name, - 1 - ); - - zend_string_release(attribute_name); - - ZVAL_LONG( - &attribute->args[0].value, - ZEND_ATTRIBUTE_TARGET_FUNCTION | ZEND_ATTRIBUTE_TARGET_METHOD - ); - - sentry_instrumented_attribute_lcname = zend_string_init_interned( - "sentry\\instrumented", - sizeof("sentry\\instrumented") - 1, - 1 - ); - - zend_observer_fcall_register(sentry_tracing_observer); - - return SUCCESS; -} - -PHP_RINIT_FUNCTION(sentry_tracing) { - zend_hash_init(&sentry_tracing_hook_keys, 8, NULL, ZVAL_PTR_DTOR, 0); - zend_hash_init(&sentry_tracing_active_calls, 8, NULL, sentry_tracing_call_state_dtor, 0); - - ZVAL_UNDEF(&sentry_tracing_start_callback); - ZVAL_UNDEF(&sentry_tracing_end_callback); - - return SUCCESS; -} - -PHP_RSHUTDOWN_FUNCTION(sentry_tracing) { - zend_hash_destroy(&sentry_tracing_hook_keys); - zend_hash_destroy(&sentry_tracing_active_calls); - - if (!Z_ISUNDEF(sentry_tracing_start_callback)) { - zval_ptr_dtor(&sentry_tracing_start_callback); - ZVAL_UNDEF(&sentry_tracing_start_callback); - } - - if (!Z_ISUNDEF(sentry_tracing_end_callback)) { - zval_ptr_dtor(&sentry_tracing_end_callback); - ZVAL_UNDEF(&sentry_tracing_end_callback); - } - - return SUCCESS; -} - -PHP_MSHUTDOWN_FUNCTION(sentry_tracing) { - if (sentry_instrumented_attribute_lcname != NULL) { - zend_string_release(sentry_instrumented_attribute_lcname); - sentry_instrumented_attribute_lcname = NULL; - } - - return SUCCESS; -} - - -static const zend_function_entry sentry_tracing_functions[] = { - ZEND_NS_FENTRY("Sentry\\Instrumentation", hook, ZEND_FN(sentry_tracing_hook), arginfo_sentry_tracing_hook_key, 0) - ZEND_NS_FENTRY("Sentry\\Instrumentation", setEndCallback, ZEND_FN(sentry_tracing_set_end_callback), arginfo_sentry_tracing_set_end_callback, 0) - ZEND_NS_FENTRY("Sentry\\Instrumentation", setStartCallback, ZEND_FN(sentry_tracing_set_start_callback), arginfo_sentry_tracing_set_start_callback, 0) - PHP_FE_END -}; - -zend_module_entry sentry_module_entry = { - STANDARD_MODULE_HEADER, - "sentry", - sentry_tracing_functions, - PHP_MINIT(sentry_tracing), - PHP_MSHUTDOWN(sentry_tracing), - PHP_RINIT(sentry_tracing), - PHP_RSHUTDOWN(sentry_tracing), - NULL, - "0.1.0", - STANDARD_MODULE_PROPERTIES -}; - -ZEND_GET_MODULE(sentry); - diff --git a/sentry.c b/sentry.c index c7c7c9f..bf8f78c 100644 --- a/sentry.c +++ b/sentry.c @@ -1,75 +1,645 @@ #ifdef HAVE_CONFIG_H #include "config.h" #endif - #include "php.h" -#include "php_sentry.h" +#include "Zend/zend_observer.h" +#include "Zend/zend_exceptions.h" +#include "Zend/zend_attributes.h" +#include + #include "sentry_arginfo.h" -#include "zend_observer.h" -#include "zend_closures.h" +#ifdef PHP_WIN32 +#include "win32/time.h" +#else +#include +#endif + +ZEND_BEGIN_MODULE_GLOBALS(sentry) + // Functions that should be observed. Values are the metadata arrays per instrumented call. + HashTable instrumented_functions; + + // Call state of currently executing functions + HashTable active_calls; + + // User callback that is invoked when an observed call begins. The return value is stored + // and passed to the end callback. + zval start_callback; + + // User callback that is invoked when an observed call ends. + zval end_callback; + + // True when currently in a callback. Used as reentry guard to prevent recursion when + // observed calls are invoked in the callback. + bool in_callback; +ZEND_END_MODULE_GLOBALS(sentry) + +ZEND_DECLARE_MODULE_GLOBALS(sentry) + +#define SENTRY_G(v) ZEND_MODULE_GLOBALS_ACCESSOR(sentry, v) + +#if defined(ZTS) && defined(COMPILE_DL_SENTRY) +ZEND_TSRMLS_CACHE_DEFINE(); +#endif + +/** + * Holds the name of the \Sentry\Trace attribute that can be used to retrieve it + * using zend_get_attribute. + */ +static zend_string *sentry_trace_attribute_lcname; + + +// ==== CALL STATE BEGIN ==== + +/** + * Tracks the state per function/method call. We need this to calculate how long an invocation took + * and to produce proper spans + */ +typedef struct { + // the name of the function to be traced, never NULL + zend_string *name; + // the wall-clock start time as unix timestamp + double start_time; + // monotonic start time used to calculate durations + zend_hrtime_t start_hrtime; + zval user_state; + zval metadata; +} sentry_call_state; -PHP_FUNCTION(sentry) { - ZEND_PARSE_PARAMETERS_NONE(); +/** + * Destructor for the call state struct + */ +static void sentry_call_state_dtor(zval *zv) { + sentry_call_state *state = Z_PTR_P(zv); - php_printf("The extension %s is loaded and working!\r\n", PHP_SENTRY_EXTNAME); + zend_string_release(state->name); + zval_ptr_dtor(&state->user_state); + zval_ptr_dtor(&state->metadata); + + efree(state); } -PHP_FUNCTION(Sentry_trace) { - zend_string *class; - zend_string *function; - zval *closure = NULL; +// ==== CALL STATE END ==== + +// ==== ATTRIBUTE START ==== +static zend_attribute *sentry_get_trace_attribute(zend_execute_data *execute_data) { + const zend_function *func = execute_data->func; + + if (func->common.attributes == NULL) { + return NULL; + } - ZEND_PARSE_PARAMETERS_START(2, 4) - Z_PARAM_STR_OR_NULL(class) - Z_PARAM_STR(function) - Z_PARAM_OBJECT_OF_CLASS_OR_NULL(closure, zend_ce_closure) + zend_attribute *attribute = zend_get_attribute( + func->common.attributes, + sentry_trace_attribute_lcname + ); + + return attribute; +} + +static bool sentry_has_trace_attribute(zend_execute_data *execute_data) { + return sentry_get_trace_attribute(execute_data) != NULL; +} + +static void sentry_get_attribute_metadata( + zend_execute_data *execute_data, + zval *metadata +) { + array_init(metadata); + + zend_attribute *attribute = sentry_get_trace_attribute(execute_data); + if (attribute != NULL && attribute->argc > 0) { + zval attribute_arg; + ZVAL_UNDEF(&attribute_arg); + + if (zend_get_attribute_value( + &attribute_arg, + attribute, + 0, + execute_data->func->common.scope + ) == SUCCESS && Z_TYPE(attribute_arg) == IS_ARRAY) { + zend_hash_copy( + Z_ARRVAL_P(metadata), + Z_ARRVAL(attribute_arg), + zval_add_ref + ); + } + + zend_object *ex = EG(exception); + if (ex != NULL) { + EG(exception) = NULL; + OBJ_RELEASE(ex); + } + + if (!Z_ISUNDEF(attribute_arg)) { + zval_ptr_dtor(&attribute_arg); + } + } +} + + +ZEND_METHOD(Sentry_Trace, __construct) { + zval *metadata = NULL; + + ZEND_PARSE_PARAMETERS_START(0, 1) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY(metadata) ZEND_PARSE_PARAMETERS_END(); +} + +// ==== ATTRIBUTE END ====== + +// ===== EXCEPTION ISOLATION START === + +// Stores exception and opline information before invoking the start or end callback. +// We do that so that the callback runs without any interference from exceptions that might +// be set from code before. +typedef struct { + zend_object *exception; + zend_object *prev_exception; + const zend_op *opline_before_exception; + bool has_opline; + const zend_op *opline; +} sentry_exception_state; + +static void sentry_exception_isolation_start(sentry_exception_state *state) { + state->exception = EG(exception); + state->prev_exception = EG(prev_exception); + state->opline_before_exception = EG(opline_before_exception); - RETURN_BOOL(add_tracer(class, function, closure)); + EG(exception) = NULL; + EG(prev_exception) = NULL; + EG(opline_before_exception) = NULL; + + const zend_execute_data *execute_data = EG(current_execute_data); + state->has_opline = execute_data != NULL; + state->opline = execute_data ? execute_data->opline : NULL; } -PHP_RINIT_FUNCTION(sentry) { -#if defined(ZTS) && defined(COMPILE_DL_OPENTELEMETRY) - ZEND_TSRMLS_CACHE_UPDATE(); -#endif +static zend_object *sentry_exception_isolation_end(sentry_exception_state *state) { + zend_object *suppressed = EG(exception); - // tracer_init(); + // exit() unwinds via a fake exception that must not be intercepted: leave it + // pending so the engine keeps tearing down the stack, and abandon the saved + // exception — nothing will ever catch it, so we release our references here. + if (UNEXPECTED(suppressed && zend_is_unwind_exit(suppressed))) { + if (state->exception != NULL) { + OBJ_RELEASE(state->exception); + } + if (state->prev_exception != NULL) { + OBJ_RELEASE(state->prev_exception); + } + return NULL; + } - return SUCCESS; + // Detach the exception the callback itself may have thrown: it lives on in + // `suppressed` and the caller owns and releases it. It must not stay in + // EG(exception), where it would mask the original exception we are about to + // restore. + // We have to set it to NULL otherwise zend_clear_exception will invoke + // the exception handler. + EG(exception) = NULL; + zend_clear_exception(); + + EG(exception) = state->exception; + EG(prev_exception) = state->prev_exception; + EG(opline_before_exception) = state->opline_before_exception; + + zend_execute_data *execute_data = EG(current_execute_data); + if (execute_data != NULL && state->has_opline) { + execute_data->opline = state->opline; + } + + return suppressed; } -PHP_RSHUTDOWN_FUNCTION(sentry) { - // tracer_cleanup(); +static void sentry_call_user_function_isolated( + zval *callback, + zval *retval, + uint32_t param_count, + zval *params +) { + SENTRY_G(in_callback) = true; - return SUCCESS; + sentry_exception_state state; + sentry_exception_isolation_start(&state); + + call_user_function( + EG(function_table), + NULL, + callback, + retval, + param_count, + params + ); + + zend_object *suppressed = sentry_exception_isolation_end(&state); + if (suppressed != NULL) { + OBJ_RELEASE(suppressed); + } + + SENTRY_G(in_callback) = false; +} + +// ===== EXCEPTION ISOLATION END ==== + +static zend_string *sentry_to_key(zend_string *name) { + if (name == NULL) { + return NULL; + } + return zend_string_tolower(name); +} + +static zend_string *sentry_build_display_name(zend_string *class_name, zend_string *function_name) { + if (function_name == NULL) { + return NULL; + } + + if (class_name == NULL) { + return zend_string_copy(function_name); + } + return zend_strpprintf(0, "%s::%s", ZSTR_VAL(class_name), ZSTR_VAL(function_name)); +} + +static zend_string *sentry_build_key(zend_string *class_name, zend_string *function_name) { + zend_string *name = sentry_build_display_name(class_name, function_name); + if (name == NULL) { + return NULL; + } + zend_string *key = sentry_to_key(name); + zend_string_release(name); + + return key; +} + +ZEND_FUNCTION(Sentry_instrument) { + zend_string *class_name = NULL; + zend_string *function_name; + zval metadata; + zval *extra_metadata = NULL; + + ZEND_PARSE_PARAMETERS_START(2,3) + Z_PARAM_STR_OR_NULL(class_name) + Z_PARAM_STR(function_name) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY(extra_metadata) + ZEND_PARSE_PARAMETERS_END(); + + // If a subclass doesn't override a method from the parent, the scope will + // remain of the parent. For example, if A defined method food and B extends A + // without overriding, doing (new B())->foo() will show up as A::foo in the + // extension. This means that declaring an instrumentation on B::foo will never + // trigger. The code below changes the classname so that it will correctly work + // for subclasses. + if (class_name != NULL) { + zend_class_entry *ce = zend_lookup_class(class_name); + if (ce != NULL) { + zend_string *lc_func = zend_string_tolower(function_name); + zend_function *func = zend_hash_find_ptr(&ce->function_table, lc_func); + if (func != NULL && func->common.scope != NULL) { + class_name = func->common.scope->name; + } + } + } + + array_init(&metadata); + + if (extra_metadata != NULL) { + zend_hash_copy( + Z_ARRVAL(metadata), + Z_ARRVAL_P(extra_metadata), + zval_add_ref + ); + } + + zend_string *key = sentry_build_key(class_name, function_name); + + const zval* inserted = zend_hash_add(&SENTRY_G(instrumented_functions), key, &metadata); + + // If the element wasn't inserted we have to manually destroy the local value to prevent memory leaks + if (inserted == NULL) { + zval_ptr_dtor(&metadata); + } + + zend_string_release(key); + + RETURN_BOOL(inserted != NULL); +} + + + + +ZEND_FUNCTION(Sentry_setEndCallback) { + zval *callback; + + ZEND_PARSE_PARAMETERS_START(1,1) + Z_PARAM_ZVAL(callback) + ZEND_PARSE_PARAMETERS_END(); + + if (!zend_is_callable(callback, 0, NULL)) { + zend_argument_type_error(1, "must be a valid callback"); + RETURN_THROWS(); + } + + if (!Z_ISUNDEF(SENTRY_G(end_callback))) { + zval_ptr_dtor(&SENTRY_G(end_callback)); + } + + ZVAL_COPY(&SENTRY_G(end_callback), callback); + + RETURN_TRUE; +} + +ZEND_FUNCTION(Sentry_setStartCallback) { + zval *callback; + + ZEND_PARSE_PARAMETERS_START(1,1) + Z_PARAM_ZVAL(callback) + ZEND_PARSE_PARAMETERS_END(); + + if (!zend_is_callable(callback, 0, NULL)) { + zend_argument_type_error(1, "must be a valid callback"); + RETURN_THROWS(); + } + + if (!Z_ISUNDEF(SENTRY_G(start_callback))) { + zval_ptr_dtor(&SENTRY_G(start_callback)); + } + + ZVAL_COPY(&SENTRY_G(start_callback), callback); + + RETURN_TRUE; +} + +static bool sentry_should_observe(zend_execute_data *execute_data) { + zend_class_entry *caller_class = execute_data->func->common.scope; + + zend_string *key = sentry_build_key( + caller_class == NULL ? NULL : caller_class->name, + execute_data->func->common.function_name + ); + + if (key == NULL) { + return false; + } + + const bool explicitly_instrumented = zend_hash_exists(&SENTRY_G(instrumented_functions), key); + + zend_string_release(key); + + if (explicitly_instrumented) { + return true; + } + + return sentry_has_trace_attribute(execute_data); +} + +static void sentry_observer_begin(zend_execute_data *execute_data) { + if (SENTRY_G(in_callback)) { + return; + } + + zend_class_entry *caller_class = zend_get_called_scope(execute_data); + + zend_string *name = sentry_build_display_name( + caller_class == NULL ? NULL : caller_class->name, + execute_data->func->common.function_name + ); + + zend_string *key = sentry_build_key( + execute_data->func->common.scope == NULL ? NULL : execute_data->func->common.scope->name, + execute_data->func->common.function_name + ); + zval *metadata = zend_hash_find(&SENTRY_G(instrumented_functions), key); + zend_string_release(key); + + zval attribute_metadata; + bool using_attribute_metadata = false; + + if (metadata == NULL) { + sentry_get_attribute_metadata(execute_data, &attribute_metadata); + metadata = &attribute_metadata; + using_attribute_metadata = true; + } + + zval retval; + ZVAL_UNDEF(&retval); + + struct timeval tv; + (void) gettimeofday(&tv, NULL); + + sentry_call_state *state = emalloc(sizeof(sentry_call_state)); + state->name = name; + state->start_time = tv.tv_sec + tv.tv_usec / 1000000.0; + state->start_hrtime = zend_hrtime(); + + ZVAL_COPY(&state->metadata, metadata); + + if (!Z_ISUNDEF(SENTRY_G(start_callback))) { + zval data; + array_init(&data); + + add_assoc_str(&data, "name", zend_string_copy(name)); + add_assoc_double(&data, "start_time", state->start_time); + + zval metadata_zv; + ZVAL_COPY(&metadata_zv, metadata); + add_assoc_zval(&data, "metadata", &metadata_zv); + + zval params[1]; + ZVAL_COPY_VALUE(¶ms[0], &data); + + sentry_call_user_function_isolated( + &SENTRY_G(start_callback), + &retval, + 1, + params + ); + + zval_ptr_dtor(¶ms[0]); + } + + if (Z_ISUNDEF(retval)) { + ZVAL_NULL(&retval); + } + state->user_state = retval; + + zval state_zv; + ZVAL_PTR(&state_zv, state); + + zend_hash_index_update(&SENTRY_G(active_calls), (zend_ulong) (uintptr_t) execute_data, &state_zv); + + if (using_attribute_metadata) { + zval_ptr_dtor(&attribute_metadata); + } +} + + +static void sentry_observer_end(zend_execute_data *execute_data, zval *return_value) { + zend_ulong hash_key = (zend_ulong) (uintptr_t) execute_data; + + zval *state_zv = zend_hash_index_find(&SENTRY_G(active_calls), hash_key); + + if (state_zv == NULL) { + return; + } + + sentry_call_state *state = Z_PTR_P(state_zv); + + zend_hrtime_t elapsed_ns = zend_hrtime() - state->start_hrtime; + double duration = elapsed_ns / 1000000.0; + double end_time = state->start_time + (duration / 1000.0); + + if (!Z_ISUNDEF(SENTRY_G(end_callback))) { + zval event; + zval retval; + ZVAL_UNDEF(&retval); + zval params[2]; + + array_init(&event); + + add_assoc_str(&event, "name", zend_string_copy(state->name)); + add_assoc_double(&event, "start_time", state->start_time); + add_assoc_double(&event, "end_time", end_time); + add_assoc_double(&event, "duration", duration); + + zval metadata_zv; + ZVAL_COPY(&metadata_zv, &state->metadata); + add_assoc_zval(&event, "metadata", &metadata_zv); + + ZVAL_COPY_VALUE(¶ms[0], &event); + + ZVAL_COPY_VALUE(¶ms[1], &state->user_state); + + sentry_call_user_function_isolated( + &SENTRY_G(end_callback), + &retval, + 2, + params + ); + + if (!Z_ISUNDEF(retval)) { + zval_ptr_dtor(&retval); + } + + zval_ptr_dtor(&event); + } + + zend_hash_index_del( + &SENTRY_G(active_calls), + hash_key + ); +} + +static zend_observer_fcall_handlers sentry_observer(zend_execute_data *execute_data) { + zend_observer_fcall_handlers handlers = {0}; + + if (sentry_should_observe(execute_data)) { + handlers.begin = sentry_observer_begin; + handlers.end = sentry_observer_end; + } + + return handlers; } PHP_MINIT_FUNCTION(sentry) { -#if defined(ZTS) && defined(COMPILE_DL_SENTRY) + zend_class_entry ce; + + INIT_NS_CLASS_ENTRY( + ce, + "Sentry", + "Trace", + class_Sentry_Trace_methods + ); + + zend_class_entry *sentry_trace_attribute_ce = zend_register_internal_class(&ce); + sentry_trace_attribute_ce->ce_flags |= ZEND_ACC_FINAL; + + zend_string *attribute_name = zend_string_init_interned( + "Attribute", + sizeof("Attribute")-1, + 1 + ); + + zend_attribute *attribute = zend_add_class_attribute( + sentry_trace_attribute_ce, + attribute_name, + 1 + ); + + zend_string_release(attribute_name); + + ZVAL_LONG( + &attribute->args[0].value, + ZEND_ATTRIBUTE_TARGET_FUNCTION | ZEND_ATTRIBUTE_TARGET_METHOD + ); + + sentry_trace_attribute_lcname = zend_string_init_interned( + "sentry\\trace", + sizeof("sentry\\trace") - 1, + 1 + ); + + zend_observer_fcall_register(sentry_observer); + + return SUCCESS; +} + +static PHP_GINIT_FUNCTION(sentry) { +#if defined(COMPILE_DL_SENTRY) && defined(ZTS) ZEND_TSRMLS_CACHE_UPDATE(); #endif + memset(sentry_globals, 0, sizeof(*sentry_globals)); +} + +PHP_RINIT_FUNCTION(sentry) { + SENTRY_G(in_callback) = false; + zend_hash_init(&SENTRY_G(instrumented_functions), 8, NULL, ZVAL_PTR_DTOR, 0); + zend_hash_init(&SENTRY_G(active_calls), 8, NULL, sentry_call_state_dtor, 0); - // zend_observer_fcall_register(); + ZVAL_UNDEF(&SENTRY_G(start_callback)); + ZVAL_UNDEF(&SENTRY_G(end_callback)); return SUCCESS; } +PHP_RSHUTDOWN_FUNCTION(sentry) { + zend_hash_destroy(&SENTRY_G(instrumented_functions)); + zend_hash_destroy(&SENTRY_G(active_calls)); + + if (!Z_ISUNDEF(SENTRY_G(start_callback))) { + zval_ptr_dtor(&SENTRY_G(start_callback)); + ZVAL_UNDEF(&SENTRY_G(start_callback)); + } + + if (!Z_ISUNDEF(SENTRY_G(end_callback))) { + zval_ptr_dtor(&SENTRY_G(end_callback)); + ZVAL_UNDEF(&SENTRY_G(end_callback)); + } + + return SUCCESS; +} + +PHP_MSHUTDOWN_FUNCTION(sentry) { + return SUCCESS; +} + zend_module_entry sentry_module_entry = { STANDARD_MODULE_HEADER, - PHP_SENTRY_EXTNAME, + "sentry", ext_functions, - PHP_MINIT(sentry), /* PHP_MINIT - Module initialization */ - NULL, /* PHP_MSHUTDOWN - Module shutdown */ - PHP_RINIT(sentry), /* PHP_RINIT - Request initialization */ - PHP_RSHUTDOWN(sentry), /* PHP_RSHUTDOWN - Request shutdown */ - NULL, /* PHP_MINFO - Module info */ - PHP_SENTRY_VERSION, - STANDARD_MODULE_PROPERTIES + PHP_MINIT(sentry), + PHP_MSHUTDOWN(sentry), + PHP_RINIT(sentry), + PHP_RSHUTDOWN(sentry), + NULL, + "0.1.0", + PHP_MODULE_GLOBALS(sentry), + PHP_GINIT(sentry), + NULL, + NULL, + STANDARD_MODULE_PROPERTIES_EX }; #ifdef COMPILE_DL_SENTRY -#ifdef ZTS -ZEND_TSRMLS_CACHE_DEFINE() -#endif -ZEND_GET_MODULE(sentry) -#endif +ZEND_GET_MODULE(sentry); +#endif \ No newline at end of file diff --git a/sentry.stub.php b/sentry.stub.php index 715b203..154f394 100644 --- a/sentry.stub.php +++ b/sentry.stub.php @@ -1,18 +1,22 @@ Date: Sun, 14 Jun 2026 14:54:01 +0200 Subject: [PATCH 03/14] update arginfo --- sentry_arginfo.h | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/sentry_arginfo.h b/sentry_arginfo.h index a19e3f1..483009b 100644 --- a/sentry_arginfo.h +++ b/sentry_arginfo.h @@ -1,20 +1,35 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: f7b643bda50e8238549b1d7d38f2a9dd9645e616 */ + * Stub hash: 0aec23779b930567dc122353b56e89b7bfb87520 */ -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_Sentry_trace, 0, 3, _IS_BOOL, 0) - ZEND_ARG_TYPE_INFO(0, class, IS_STRING, 1) - ZEND_ARG_TYPE_INFO(0, function, IS_STRING, 0) - ZEND_ARG_OBJ_INFO(0, closure, Closure, 0) +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_Sentry_instrument, 0, 2, _IS_BOOL, 0) + ZEND_ARG_TYPE_INFO(0, class_name, IS_STRING, 1) + ZEND_ARG_TYPE_INFO(0, function_name, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, extra_metadata, IS_ARRAY, 0, "[]") ZEND_END_ARG_INFO() -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_sentry, 0, 0, IS_VOID, 0) +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_Sentry_setEndCallback, 0, 1, _IS_BOOL, 0) + ZEND_ARG_TYPE_INFO(0, callback, IS_CALLABLE, 0) ZEND_END_ARG_INFO() -ZEND_FUNCTION(Sentry_trace); -ZEND_FUNCTION(sentry); +#define arginfo_Sentry_setStartCallback arginfo_Sentry_setEndCallback + +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Sentry_Trace___construct, 0, 0, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, metadata, IS_ARRAY, 0, "[]") +ZEND_END_ARG_INFO() + +ZEND_FUNCTION(Sentry_instrument); +ZEND_FUNCTION(Sentry_setEndCallback); +ZEND_FUNCTION(Sentry_setStartCallback); +ZEND_METHOD(Sentry_Trace, __construct); static const zend_function_entry ext_functions[] = { - ZEND_RAW_FENTRY(ZEND_NS_NAME("Sentry", "trace"), zif_Sentry_trace, arginfo_Sentry_trace, 0, NULL, NULL) - ZEND_FE(sentry, arginfo_sentry) + ZEND_RAW_FENTRY(ZEND_NS_NAME("Sentry", "instrument"), zif_Sentry_instrument, arginfo_Sentry_instrument, 0, NULL, NULL) + ZEND_RAW_FENTRY(ZEND_NS_NAME("Sentry", "setEndCallback"), zif_Sentry_setEndCallback, arginfo_Sentry_setEndCallback, 0, NULL, NULL) + ZEND_RAW_FENTRY(ZEND_NS_NAME("Sentry", "setStartCallback"), zif_Sentry_setStartCallback, arginfo_Sentry_setStartCallback, 0, NULL, NULL) + ZEND_FE_END +}; + +static const zend_function_entry class_Sentry_Trace_methods[] = { + ZEND_ME(Sentry_Trace, __construct, arginfo_class_Sentry_Trace___construct, ZEND_ACC_PUBLIC) ZEND_FE_END }; From 7f5c89ad8652812839979bb3402148dc707b27f9 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Sun, 14 Jun 2026 15:17:49 +0200 Subject: [PATCH 04/14] update arginfo and add tests --- sentry.stub.php | 1 + sentry_arginfo.h | 14 ++++++- tests/test_callback_return_value.phpt | 34 +++++++++++++++ tests/test_case_insensitive.phpt | 23 +++++++++++ tests/test_duplicate_instrument.phpt | 28 +++++++++++++ tests/test_exception_in_end_callback.phpt | 20 +++++++++ ...st_exception_in_function_and_callback.phpt | 31 ++++++++++++++ tests/test_exception_in_instrumented.phpt | 29 +++++++++++++ tests/test_exception_in_start_callback.phpt | 20 +++++++++ tests/test_function_metadata.phpt | 37 +++++++++++++++++ tests/test_function_metadata_attribute.phpt | 37 +++++++++++++++++ tests/test_inheritance_correct_name.phpt | 33 +++++++++++++++ ...st_inheritance_correct_name_attribute.phpt | 32 +++++++++++++++ tests/test_method_metadata.phpt | 34 +++++++++++++++ tests/test_method_metadata_attribute.phpt | 34 +++++++++++++++ tests/test_nested_functions.phpt | 41 +++++++++++++++++++ tests/test_no_callback.phpt | 16 ++++++++ tests/test_non_existent_function.phpt | 21 ++++++++++ tests/test_non_existent_method.phpt | 23 +++++++++++ tests/test_reentry_guard.phpt | 27 ++++++++++++ tests/test_simple_function.phpt | 28 +++++++++++++ tests/test_simple_function_attribute.phpt | 28 +++++++++++++ tests/test_simple_method.phpt | 25 +++++++++++ tests/test_simple_method_attribute.phpt | 25 +++++++++++ tests/test_static_function_attribute.phpt | 25 +++++++++++ tests/test_static_method.phpt | 25 +++++++++++ 26 files changed, 690 insertions(+), 1 deletion(-) create mode 100644 tests/test_callback_return_value.phpt create mode 100644 tests/test_case_insensitive.phpt create mode 100644 tests/test_duplicate_instrument.phpt create mode 100644 tests/test_exception_in_end_callback.phpt create mode 100644 tests/test_exception_in_function_and_callback.phpt create mode 100644 tests/test_exception_in_instrumented.phpt create mode 100644 tests/test_exception_in_start_callback.phpt create mode 100644 tests/test_function_metadata.phpt create mode 100644 tests/test_function_metadata_attribute.phpt create mode 100644 tests/test_inheritance_correct_name.phpt create mode 100644 tests/test_inheritance_correct_name_attribute.phpt create mode 100644 tests/test_method_metadata.phpt create mode 100644 tests/test_method_metadata_attribute.phpt create mode 100644 tests/test_nested_functions.phpt create mode 100644 tests/test_no_callback.phpt create mode 100644 tests/test_non_existent_function.phpt create mode 100644 tests/test_non_existent_method.phpt create mode 100644 tests/test_reentry_guard.phpt create mode 100644 tests/test_simple_function.phpt create mode 100644 tests/test_simple_function_attribute.phpt create mode 100644 tests/test_simple_method.phpt create mode 100644 tests/test_simple_method_attribute.phpt create mode 100644 tests/test_static_function_attribute.phpt create mode 100644 tests/test_static_method.phpt diff --git a/sentry.stub.php b/sentry.stub.php index 154f394..7c8cf63 100644 --- a/sentry.stub.php +++ b/sentry.stub.php @@ -2,6 +2,7 @@ /** * @generate-function-entries + * @generate-legacy-arginfo 80000 */ namespace Sentry { diff --git a/sentry_arginfo.h b/sentry_arginfo.h index 483009b..f240b84 100644 --- a/sentry_arginfo.h +++ b/sentry_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 0aec23779b930567dc122353b56e89b7bfb87520 */ + * Stub hash: 421599558cb4157fb6c73190d9be80ca63fa0bad */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_Sentry_instrument, 0, 2, _IS_BOOL, 0) ZEND_ARG_TYPE_INFO(0, class_name, IS_STRING, 1) @@ -23,9 +23,21 @@ ZEND_FUNCTION(Sentry_setStartCallback); ZEND_METHOD(Sentry_Trace, __construct); static const zend_function_entry ext_functions[] = { +#if (PHP_VERSION_ID >= 80400) ZEND_RAW_FENTRY(ZEND_NS_NAME("Sentry", "instrument"), zif_Sentry_instrument, arginfo_Sentry_instrument, 0, NULL, NULL) +#else + ZEND_RAW_FENTRY(ZEND_NS_NAME("Sentry", "instrument"), zif_Sentry_instrument, arginfo_Sentry_instrument, 0) +#endif +#if (PHP_VERSION_ID >= 80400) ZEND_RAW_FENTRY(ZEND_NS_NAME("Sentry", "setEndCallback"), zif_Sentry_setEndCallback, arginfo_Sentry_setEndCallback, 0, NULL, NULL) +#else + ZEND_RAW_FENTRY(ZEND_NS_NAME("Sentry", "setEndCallback"), zif_Sentry_setEndCallback, arginfo_Sentry_setEndCallback, 0) +#endif +#if (PHP_VERSION_ID >= 80400) ZEND_RAW_FENTRY(ZEND_NS_NAME("Sentry", "setStartCallback"), zif_Sentry_setStartCallback, arginfo_Sentry_setStartCallback, 0, NULL, NULL) +#else + ZEND_RAW_FENTRY(ZEND_NS_NAME("Sentry", "setStartCallback"), zif_Sentry_setStartCallback, arginfo_Sentry_setStartCallback, 0) +#endif ZEND_FE_END }; diff --git a/tests/test_callback_return_value.phpt b/tests/test_callback_return_value.phpt new file mode 100644 index 0000000..e9d90bf --- /dev/null +++ b/tests/test_callback_return_value.phpt @@ -0,0 +1,34 @@ +--TEST-- +Tests that any value returned by the start callback is returned to the end callback as second parameter. +--EXTENSIONS-- +sentry +--FILE-- + +--EXPECTF-- +Name: test_instrumented +Duration: %f +This value is passed to the end callback as second param \ No newline at end of file diff --git a/tests/test_case_insensitive.phpt b/tests/test_case_insensitive.phpt new file mode 100644 index 0000000..2de76f6 --- /dev/null +++ b/tests/test_case_insensitive.phpt @@ -0,0 +1,23 @@ +--TEST-- +Tests that instrumentation can be registered case insensitive and will still capture the correctly cased name. +--EXTENSIONS-- +sentry +--FILE-- + +--EXPECTF-- +Name: work +Duration: %f \ No newline at end of file diff --git a/tests/test_duplicate_instrument.phpt b/tests/test_duplicate_instrument.phpt new file mode 100644 index 0000000..a3e1f1e --- /dev/null +++ b/tests/test_duplicate_instrument.phpt @@ -0,0 +1,28 @@ +--TEST-- +Tests that registering the first time for instrumentation returns true and false on subsequent calls to `\Sentry\instrument`. +--EXTENSIONS-- +sentry +--FILE-- + +--EXPECTF-- +First result: true +Second result: false +Name: work +Duration: %f \ No newline at end of file diff --git a/tests/test_exception_in_end_callback.phpt b/tests/test_exception_in_end_callback.phpt new file mode 100644 index 0000000..cc547d3 --- /dev/null +++ b/tests/test_exception_in_end_callback.phpt @@ -0,0 +1,20 @@ +--TEST-- +Tests that exceptions thrown in the end callback will be swallowed silently and not break userland applications. +--EXTENSIONS-- +sentry +--FILE-- + 'test']); +work(); + +?> +--EXPECTF-- \ No newline at end of file diff --git a/tests/test_exception_in_function_and_callback.phpt b/tests/test_exception_in_function_and_callback.phpt new file mode 100644 index 0000000..f045728 --- /dev/null +++ b/tests/test_exception_in_function_and_callback.phpt @@ -0,0 +1,31 @@ +--TEST-- +Tests that if a function and the callback throw, the callback exception is not leaked and the original exception +can be recovered. +--EXTENSIONS-- +sentry +--FILE-- +getMessage() . PHP_EOL; +} + +?> +--EXPECTF-- +Name: work +Duration: %f +boom \ No newline at end of file diff --git a/tests/test_exception_in_instrumented.phpt b/tests/test_exception_in_instrumented.phpt new file mode 100644 index 0000000..8aa9103 --- /dev/null +++ b/tests/test_exception_in_instrumented.phpt @@ -0,0 +1,29 @@ +--TEST-- +Tests that exceptions thrown in instrumented functions do not interfere with the end callback. +--EXTENSIONS-- +sentry +--FILE-- + 'test']); +try { + work(); +} catch (Throwable $t) { + +} + +?> +--EXPECTF-- +Name: work +Duration: %f +Metadata: test \ No newline at end of file diff --git a/tests/test_exception_in_start_callback.phpt b/tests/test_exception_in_start_callback.phpt new file mode 100644 index 0000000..35ca8cc --- /dev/null +++ b/tests/test_exception_in_start_callback.phpt @@ -0,0 +1,20 @@ +--TEST-- +Tests that exceptions thrown in the start callback will be swallowed silently and not break userland applications. +--EXTENSIONS-- +sentry +--FILE-- + 'test']); +work(); + +?> +--EXPECTF-- \ No newline at end of file diff --git a/tests/test_function_metadata.phpt b/tests/test_function_metadata.phpt new file mode 100644 index 0000000..3df42f1 --- /dev/null +++ b/tests/test_function_metadata.phpt @@ -0,0 +1,37 @@ +--TEST-- +Tests that metadata declared in `instrument` will be passed to the callbacks. +--EXTENSIONS-- +sentry +--FILE-- + 'test']); +test_instrumented(); + +?> +--EXPECTF-- +Start name: test_instrumented +Start Metadata: test +End name: test_instrumented +End duration: %f +End metadata: test \ No newline at end of file diff --git a/tests/test_function_metadata_attribute.phpt b/tests/test_function_metadata_attribute.phpt new file mode 100644 index 0000000..fcea13b --- /dev/null +++ b/tests/test_function_metadata_attribute.phpt @@ -0,0 +1,37 @@ +--TEST-- +Tests that metadata declared in the Trace attribute will be passed to the callbacks. +--EXTENSIONS-- +sentry +--FILE-- + 'test'])] +function test_instrumented() { + $result = 0; + for($i = 0; $i < 1000; $i++) { + $result += $i; + } + + return $result; +} + +\Sentry\setStartCallback(static function (array $data) { + echo "Start name: " . $data['name'] . PHP_EOL; + echo "Start metadata: " . ($data['metadata']['sentry.op'] ?? 'invalid') . PHP_EOL; +}); + +\Sentry\setEndCallback(static function (array $data) { + echo "End name: " . $data['name'] . PHP_EOL; + echo "End duration: " . $data['duration'] . PHP_EOL; + echo "End metadata: " . ($data['metadata']['sentry.op'] ?? 'invalid') . PHP_EOL; +}); + +test_instrumented(); + +?> +--EXPECTF-- +Start name: test_instrumented +Start metadata: test +End name: test_instrumented +End duration: %f +End metadata: test \ No newline at end of file diff --git a/tests/test_inheritance_correct_name.phpt b/tests/test_inheritance_correct_name.phpt new file mode 100644 index 0000000..44e3303 --- /dev/null +++ b/tests/test_inheritance_correct_name.phpt @@ -0,0 +1,33 @@ +--TEST-- +Tests that inherited functions that are not overwritten can be traced using `\Sentry\instrument`. +--EXTENSIONS-- +sentry +--FILE-- +work(); +(new A())->work(); + +?> +--EXPECTF-- +Name: B::work +Duration: %f +Name: A::work +Duration: %f \ No newline at end of file diff --git a/tests/test_inheritance_correct_name_attribute.phpt b/tests/test_inheritance_correct_name_attribute.phpt new file mode 100644 index 0000000..39040d8 --- /dev/null +++ b/tests/test_inheritance_correct_name_attribute.phpt @@ -0,0 +1,32 @@ +--TEST-- +Tests that inherited functions that are not overwritten can be traced using `\Sentry\Trace` attribute. +--EXTENSIONS-- +sentry +--FILE-- +work(); +(new A())->work(); + +?> +--EXPECTF-- +Name: B::work +Duration: %f +Name: A::work +Duration: %f \ No newline at end of file diff --git a/tests/test_method_metadata.phpt b/tests/test_method_metadata.phpt new file mode 100644 index 0000000..59ec6ad --- /dev/null +++ b/tests/test_method_metadata.phpt @@ -0,0 +1,34 @@ +--TEST-- +Tests that metadata declared in `instrument` will be passed to the callbacks. +--EXTENSIONS-- +sentry +--FILE-- + 'test']); +(new Foo())->work(); + +?> +--EXPECTF-- +Start name: Foo::work +Start metadata: test +End name: Foo::work +End duration: %f +End metadata: test \ No newline at end of file diff --git a/tests/test_method_metadata_attribute.phpt b/tests/test_method_metadata_attribute.phpt new file mode 100644 index 0000000..978ace8 --- /dev/null +++ b/tests/test_method_metadata_attribute.phpt @@ -0,0 +1,34 @@ +--TEST-- +Tests that metadata declared in the Trace attribute will be passed to the callbacks. +--EXTENSIONS-- +sentry +--FILE-- + 'test'])] + public function work() { + return 10 + 2000; + } +} + +\Sentry\setStartCallback(static function (array $data) { + echo "Start name: " . $data['name'] . PHP_EOL; + echo "Start metadata: " . ($data['metadata']['sentry.op'] ?? 'invalid') . PHP_EOL; +}); + +\Sentry\setEndCallback(static function (array $data) { + echo "End name: " . $data['name'] . PHP_EOL; + echo "End duration: " . $data['duration'] . PHP_EOL; + echo "End metadata: " . ($data['metadata']['sentry.op'] ?? 'invalid') . PHP_EOL; +}); + +(new Foo())->work(); + +?> +--EXPECTF-- +Start name: Foo::work +Start metadata: test +End name: Foo::work +End duration: %f +End metadata: test \ No newline at end of file diff --git a/tests/test_nested_functions.phpt b/tests/test_nested_functions.phpt new file mode 100644 index 0000000..ab39c2c --- /dev/null +++ b/tests/test_nested_functions.phpt @@ -0,0 +1,41 @@ +--TEST-- +Tests that nested calls will invoke the callbacks in the correct order. +--EXTENSIONS-- +sentry +--FILE-- + +--EXPECTF-- +Start name: work1 +Start name: work2 +Start name: work3 +End name: work3 +End name: work2 +End name: work1 \ No newline at end of file diff --git a/tests/test_no_callback.phpt b/tests/test_no_callback.phpt new file mode 100644 index 0000000..4ccf010 --- /dev/null +++ b/tests/test_no_callback.phpt @@ -0,0 +1,16 @@ +--TEST-- +Tests that if no callbacks are registered, the application will just work normally without any interference. +--EXTENSIONS-- +sentry +--FILE-- + +--EXPECTF-- diff --git a/tests/test_non_existent_function.phpt b/tests/test_non_existent_function.phpt new file mode 100644 index 0000000..85612fc --- /dev/null +++ b/tests/test_non_existent_function.phpt @@ -0,0 +1,21 @@ +--TEST-- +Tests that instrumenting a non existient function will not cause any crashes or errors. +--EXTENSIONS-- +sentry +--FILE-- + +--EXPECTF-- \ No newline at end of file diff --git a/tests/test_non_existent_method.phpt b/tests/test_non_existent_method.phpt new file mode 100644 index 0000000..0bce3cd --- /dev/null +++ b/tests/test_non_existent_method.phpt @@ -0,0 +1,23 @@ +--TEST-- +Tests that instrumenting a non existent method will not cause any crashes or errors. +--EXTENSIONS-- +sentry +--FILE-- +work(); + +?> +--EXPECTF-- \ No newline at end of file diff --git a/tests/test_reentry_guard.phpt b/tests/test_reentry_guard.phpt new file mode 100644 index 0000000..d8d280d --- /dev/null +++ b/tests/test_reentry_guard.phpt @@ -0,0 +1,27 @@ +--TEST-- +Tests that calling instrumented functions from a callback will not instrument them again to prevent +infinite recursion. +--EXTENSIONS-- +sentry +--FILE-- + 'test']); +work(); + +?> +--EXPECTF-- +Name: work +Duration: %f +Metadata: test \ No newline at end of file diff --git a/tests/test_simple_function.phpt b/tests/test_simple_function.phpt new file mode 100644 index 0000000..03d137d --- /dev/null +++ b/tests/test_simple_function.phpt @@ -0,0 +1,28 @@ +--TEST-- +Tests a regular function instrumented by instrument +--EXTENSIONS-- +sentry +--FILE-- + +--EXPECTF-- +Name: test_instrumented +Duration: %f \ No newline at end of file diff --git a/tests/test_simple_function_attribute.phpt b/tests/test_simple_function_attribute.phpt new file mode 100644 index 0000000..27f4e2a --- /dev/null +++ b/tests/test_simple_function_attribute.phpt @@ -0,0 +1,28 @@ +--TEST-- +Tests a regular function instrumented by instrument +--EXTENSIONS-- +sentry +--FILE-- + +--EXPECTF-- +Name: test_instrumented +Duration: %f \ No newline at end of file diff --git a/tests/test_simple_method.phpt b/tests/test_simple_method.phpt new file mode 100644 index 0000000..ad3ada4 --- /dev/null +++ b/tests/test_simple_method.phpt @@ -0,0 +1,25 @@ +--TEST-- +Tests a method can be instrumented using `\Sentry\instrument` +--EXTENSIONS-- +sentry +--FILE-- +work(); + +?> +--EXPECTF-- +Name: Foo::work +Duration: %f \ No newline at end of file diff --git a/tests/test_simple_method_attribute.phpt b/tests/test_simple_method_attribute.phpt new file mode 100644 index 0000000..31d7a97 --- /dev/null +++ b/tests/test_simple_method_attribute.phpt @@ -0,0 +1,25 @@ +--TEST-- +Tests a method can be instrumented using `#[\Sentry\Trace]` attribute. +--EXTENSIONS-- +sentry +--FILE-- +work(); + +?> +--EXPECTF-- +Name: Foo::work +Duration: %f \ No newline at end of file diff --git a/tests/test_static_function_attribute.phpt b/tests/test_static_function_attribute.phpt new file mode 100644 index 0000000..c8d124b --- /dev/null +++ b/tests/test_static_function_attribute.phpt @@ -0,0 +1,25 @@ +--TEST-- +Tests that static functions can be instrumented using `\Sentry\Trace` attribute +--EXTENSIONS-- +sentry +--FILE-- + +--EXPECTF-- +Name: Foo::work +Duration: %f \ No newline at end of file diff --git a/tests/test_static_method.phpt b/tests/test_static_method.phpt new file mode 100644 index 0000000..e5c8185 --- /dev/null +++ b/tests/test_static_method.phpt @@ -0,0 +1,25 @@ +--TEST-- +Tests that static functions can be instrumented using `\Sentry\instrument` +--EXTENSIONS-- +sentry +--FILE-- + +--EXPECTF-- +Name: Foo::work +Duration: %f \ No newline at end of file From 5e06313dde3e1b434611d3dc11e4d306bf173ea5 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Sun, 14 Jun 2026 15:20:16 +0200 Subject: [PATCH 05/14] add zend_hrtime --- sentry.c | 1 + 1 file changed, 1 insertion(+) diff --git a/sentry.c b/sentry.c index bf8f78c..eae1840 100644 --- a/sentry.c +++ b/sentry.c @@ -5,6 +5,7 @@ #include "Zend/zend_observer.h" #include "Zend/zend_exceptions.h" #include "Zend/zend_attributes.h" +#include "Zend/zend_hrtime.h" #include #include "sentry_arginfo.h" From 2ef4c3cdc9d10651774a7bce8bad5021dfaf4f83 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Sun, 14 Jun 2026 15:34:51 +0200 Subject: [PATCH 06/14] fix hrtime --- sentry.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sentry.c b/sentry.c index eae1840..3d04d8a 100644 --- a/sentry.c +++ b/sentry.c @@ -5,7 +5,11 @@ #include "Zend/zend_observer.h" #include "Zend/zend_exceptions.h" #include "Zend/zend_attributes.h" -#include "Zend/zend_hrtime.h" +#if PHP_VERSION_ID < 80300 +#include "ext/standard/hrtime.h" +#define zend_hrtime_t php_hrtime_t +#define zend_hrtime php_hrtime_current +#endif #include #include "sentry_arginfo.h" From 2482332ffa0885ce38522ae246cf68987b2153cc Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Sun, 14 Jun 2026 15:44:20 +0200 Subject: [PATCH 07/14] expand test matrix --- .github/workflows/tests.yml | 8 ++++++-- config.w32 | 3 +-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9c2886c..09f12f4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,18 +7,20 @@ on: jobs: linux: - name: PHP ${{ matrix.php }} (Linux) + name: PHP ${{ matrix.php }}${{ matrix.ts && ' (ZTS)' || '' }} (Linux) runs-on: ubuntu-latest strategy: fail-fast: false matrix: php: ['8.0', '8.1', '8.2', '8.3', '8.4', '8.5'] + ts: [false, true] steps: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} + php-ts: ${{ matrix.ts }} - name: Build run: | @@ -44,6 +46,7 @@ jobs: php-version: ${{ matrix.php-version }} arch: ${{ matrix.arch }} ts: ${{ matrix.ts }} + args: --enable-sentry get-windows-matrix: runs-on: ubuntu-latest @@ -55,4 +58,5 @@ jobs: - id: matrix uses: php/php-windows-builder/extension-matrix@v1 with: - php-version-list: '8.0, 8.1, 8.2, 8.3, 8.4' + php-version-list: '8.0, 8.1, 8.2, 8.3, 8.4, 8.5' + arch-list: x64 diff --git a/config.w32 b/config.w32 index 3641ff0..a6197ba 100644 --- a/config.w32 +++ b/config.w32 @@ -1,5 +1,4 @@ -ARG_ENABLE("sentry", "whether to enable Sentry support", - "Enable Sentry support"); +ARG_ENABLE("sentry", "Enable Sentry support", "no"); if (PHP_SENTRY == "yes") { AC_DEFINE("HAVE_SENTRY", 1, "Whether you have Sentry"); EXTENSION("Sentry", "sentry.c", true); From bd7a72ff0320e637e534158a64c1163edae3075f Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Sun, 14 Jun 2026 16:01:20 +0200 Subject: [PATCH 08/14] add tests --- .../test_end_callback_can_be_overwritten.phpt | 26 +++++++++++++++++++ ...est_start_callback_can_be_overwritten.phpt | 26 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 tests/test_end_callback_can_be_overwritten.phpt create mode 100644 tests/test_start_callback_can_be_overwritten.phpt diff --git a/tests/test_end_callback_can_be_overwritten.phpt b/tests/test_end_callback_can_be_overwritten.phpt new file mode 100644 index 0000000..c717c34 --- /dev/null +++ b/tests/test_end_callback_can_be_overwritten.phpt @@ -0,0 +1,26 @@ +--TEST-- +Tests that the end callback can be updated by calling setEndCallback again. +--EXTENSIONS-- +sentry +--FILE-- +work(); + +?> +--EXPECTF-- +Second callback \ No newline at end of file diff --git a/tests/test_start_callback_can_be_overwritten.phpt b/tests/test_start_callback_can_be_overwritten.phpt new file mode 100644 index 0000000..3148804 --- /dev/null +++ b/tests/test_start_callback_can_be_overwritten.phpt @@ -0,0 +1,26 @@ +--TEST-- +Tests that the start callback can be updated by calling setStartCallback again. +--EXTENSIONS-- +sentry +--FILE-- +work(); + +?> +--EXPECTF-- +Second callback \ No newline at end of file From bd41748032b2a05e9e3503b8930f22ce74c5437c Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Sun, 14 Jun 2026 16:12:03 +0200 Subject: [PATCH 09/14] array shape in stub --- sentry.stub.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sentry.stub.php b/sentry.stub.php index 7c8cf63..a40eec5 100644 --- a/sentry.stub.php +++ b/sentry.stub.php @@ -3,6 +3,9 @@ /** * @generate-function-entries * @generate-legacy-arginfo 80000 + * + * @phpstan-type StartData array{name: string, start_time: float, metadata: array} + * @phpstan-type EndData array{name: string, start_time: float, end_time: float, duration: float, metadata: array} */ namespace Sentry { @@ -12,8 +15,10 @@ function instrument( array $extra_metadata = [] ): bool {} + /** @param callable(EndData, mixed): mixed $callback */ function setEndCallback(callable $callback): bool {} + /** @param callable(StartData): mixed $callback */ function setStartCallback(callable $callback): bool {} #[\Attribute(\Attribute::TARGET_FUNCTION | \Attribute::TARGET_METHOD)] From 631ddc034a9a70030b0a9032b17a3fbb8b34a091 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Sun, 14 Jun 2026 16:21:40 +0200 Subject: [PATCH 10/14] add test --- sentry.stub.php | 10 +++-- sentry_arginfo.h | 2 +- ...est_return_is_isolated_per_invocation.phpt | 37 +++++++++++++++++++ 3 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 tests/test_return_is_isolated_per_invocation.phpt diff --git a/sentry.stub.php b/sentry.stub.php index a40eec5..c75592b 100644 --- a/sentry.stub.php +++ b/sentry.stub.php @@ -4,8 +4,6 @@ * @generate-function-entries * @generate-legacy-arginfo 80000 * - * @phpstan-type StartData array{name: string, start_time: float, metadata: array} - * @phpstan-type EndData array{name: string, start_time: float, end_time: float, duration: float, metadata: array} */ namespace Sentry { @@ -15,10 +13,14 @@ function instrument( array $extra_metadata = [] ): bool {} - /** @param callable(EndData, mixed): mixed $callback */ + /** + * @phpstan-param callable(array{name: string, start_time: float, end_time: float, duration: float, metadata: array}, mixed): mixed $callback + */ function setEndCallback(callable $callback): bool {} - /** @param callable(StartData): mixed $callback */ + /** + * @phpstan-param callable(array{name: string, start_time: float, metadata: array}): mixed $callback + */ function setStartCallback(callable $callback): bool {} #[\Attribute(\Attribute::TARGET_FUNCTION | \Attribute::TARGET_METHOD)] diff --git a/sentry_arginfo.h b/sentry_arginfo.h index f240b84..0d510b1 100644 --- a/sentry_arginfo.h +++ b/sentry_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 421599558cb4157fb6c73190d9be80ca63fa0bad */ + * Stub hash: a8f0df9a0ea7aa5af60848effeda1f1ac2952c61 */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_Sentry_instrument, 0, 2, _IS_BOOL, 0) ZEND_ARG_TYPE_INFO(0, class_name, IS_STRING, 1) diff --git a/tests/test_return_is_isolated_per_invocation.phpt b/tests/test_return_is_isolated_per_invocation.phpt new file mode 100644 index 0000000..f23e8a8 --- /dev/null +++ b/tests/test_return_is_isolated_per_invocation.phpt @@ -0,0 +1,37 @@ +--TEST-- +Tests that the value returned from the start callback is properly isolated and handled to the correct end callback. +--EXTENSIONS-- +sentry +--FILE-- + 'test1'])] +function foo() { + bar(); +} + +#[\Sentry\Trace(['test' => 'test2'])] +function bar() { + baz(); +} + +#[\Sentry\Trace(['test' => 'test3'])] +function baz() { + return 30; +} + +\Sentry\setStartCallback(static function(array $data) { + return $data['metadata']['test']; +}); + +\Sentry\setEndCallback(static function (array $data, string $userData) { + echo $userData . PHP_EOL; +}); + +foo(); + +?> +--EXPECTF-- +test3 +test2 +test1 \ No newline at end of file From 02bc9869bbffadcad50db521904d2c208302e88f Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Mon, 15 Jun 2026 11:33:28 +0200 Subject: [PATCH 11/14] fix memory leak --- sentry.c | 1 + 1 file changed, 1 insertion(+) diff --git a/sentry.c b/sentry.c index 3d04d8a..e19a6d1 100644 --- a/sentry.c +++ b/sentry.c @@ -303,6 +303,7 @@ ZEND_FUNCTION(Sentry_instrument) { if (func != NULL && func->common.scope != NULL) { class_name = func->common.scope->name; } + zend_string_release(lc_func); } } From c47420a7f04b7d678153876e5a6f0086d3d23a46 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Mon, 15 Jun 2026 11:39:37 +0200 Subject: [PATCH 12/14] readme warning --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ebdf19d..32d3799 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # sentry-php-tracer +> [!CAUTION] +> Work in progress. Expect API changes while not 1.0 + Sentry extension to enable automatic instrumentation of methods and functions. Methods/functions can be instrumented by either declaring the #[\Sentry\Trace] attribute on them @@ -53,8 +56,6 @@ Attribute: Function: `\Sentry\instrument("MyClass", "myFunction", ['my-attribute' => 'test', 'other' => 'foo']` -This is work in progress. - ## Build the extension ``` From 83668364f48e6bda829af65659c13380d48d2505 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Mon, 15 Jun 2026 14:06:45 +0200 Subject: [PATCH 13/14] add valgrind test --- .github/workflows/tests.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 09f12f4..cc21679 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -31,6 +31,28 @@ jobs: - name: Test run: make test TESTS="--show-diff" NO_INTERACTION=1 + valgrind: + name: Valgrind (Linux) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + + - name: Install valgrind + run: sudo apt-get install -y valgrind + + - name: Build + run: | + phpize + ./configure --enable-sentry + make + + - name: Test under valgrind + run: make test TESTS="--show-diff -m" NO_INTERACTION=1 + windows: name: PHP ${{ matrix.php-version }} (${{ matrix.arch }}, ${{ matrix.ts == '' && 'nts' || 'ts' }}) (Windows) needs: get-windows-matrix From d0e51d5ed7cf415e777b54a98fd5511860bfb91b Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Mon, 15 Jun 2026 15:17:21 +0200 Subject: [PATCH 14/14] casing --- config.m4 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.m4 b/config.m4 index 36795ea..d9e2d86 100644 --- a/config.m4 +++ b/config.m4 @@ -2,5 +2,5 @@ PHP_ARG_ENABLE(sentry, whether to enable Sentry support, [ --enable-sentry Enable Sentry support]) if test "$PHP_SENTRY" = "yes"; then AC_DEFINE(HAVE_SENTRY, 1, [Whether you have Sentry]) - PHP_NEW_EXTENSION(Sentry, sentry.c, $ext_shared) + PHP_NEW_EXTENSION(sentry, sentry.c, $ext_shared) fi \ No newline at end of file