From 7759b59ce982e4d856a0514dcead8358ee756d16 Mon Sep 17 00:00:00 2001 From: Sammy Kaye Powers Date: Sun, 11 Apr 2021 11:03:06 -0700 Subject: [PATCH 1/3] Add some documentation to the embed SAPI --- sapi/embed/README.md | 167 +++++++++++++++++++++++++++++++++++++++++ sapi/embed/php_embed.c | 141 ++++++++++++++++++++++------------ 2 files changed, 262 insertions(+), 46 deletions(-) create mode 100644 sapi/embed/README.md diff --git a/sapi/embed/README.md b/sapi/embed/README.md new file mode 100644 index 0000000000000..6a1b15bb5f062 --- /dev/null +++ b/sapi/embed/README.md @@ -0,0 +1,167 @@ +# The embed SAPI + +A server application programming interface (SAPI), is the entry point into the Zend Engine. The embed SAPI is a lightweight SAPI for calling into the Zend Engine from C or other languages that have C bindings. + +## Basic Example + +Below is a basic example in C that uses the embed SAPI to boot up the Zend Engine, start a request, and print the number of functions loaded in the function table. + +```c +/* embed_sapi_basic_example.c */ + +#include + +int main(int argc, char **argv) +{ + /* Invokes the Zend Engine initialization phase: SAPI (SINIT), modules + * (MINIT), and request (RINIT). It also opens a 'zend_try' block to catch + * a zend_bailout(). + */ + PHP_EMBED_START_BLOCK(argc, argv) + + php_printf( + "Number of functions loaded: %d\n", + zend_hash_num_elements(EG(function_table)) + ); + + /* Close the 'zend_try' block and invoke the shutdown phase: request + * (RSHUTDOWN), modules (MSHUTDOWN), and SAPI (SSHUTDOWN). + */ + PHP_EMBED_END_BLOCK() +} +``` + +To compile this, we must point the compiler to the PHP header files. The paths to the header files are listed from (`php-config --includes`) but the path to the SAPI header files are not included in that list by default so we must explicitly include `$(php-config --include-dir)/sapi`. + +We must also point the linker and the runtime loader to the `libphp.so` shared lib for linking PHP (`-lphp`) which is located at `$(php-config --prefix)/lib`. So the complete command to compile ends up being: + +```bash +$ gcc \ + $(php-config --includes) \ + -I$(php-config --include-dir)/sapi \ + -L$(php-config --prefix)/lib \ + embed_sapi_basic_example.c \ + -lphp \ + -Wl,-rpath=$(php-config --prefix)/lib +``` + +> :memo: The embed SAPI is disabled by default. In order for the above example to compile, PHP must be built with the embed SAPI enabled. To see what SAPIs are installed, run `php-config --php-sapis`. If you don't see `embed` in the list, you'll need to rebuild PHP with `./configure --enable-embed`. The PHP shared library `libphp.so` is built when the embed SAPI is enabled. + +If all goes to plan you should be able to run the program. + +```bash +$ ./a.out +Number of functions loaded: 1046 +``` + +## Function call example + +The following example calls `mt_rand()` and `var_dump()`s the return value. + +```c +#include
+#include +#include + +int main(int argc, char **argv) +{ + PHP_EMBED_START_BLOCK(argc, argv) + + zval retval = {0}; + zend_fcall_info fci = {0}; + zend_fcall_info_cache fci_cache = {0}; + + zend_string *func_name = zend_string_init(ZEND_STRL("mt_rand"), 0); + ZVAL_STR(&fci.function_name, func_name); + + fci.size = sizeof fci; + fci.retval = &retval; + + if (zend_call_function(&fci, &fci_cache) == SUCCESS) { + php_var_dump(&retval, 1); + } + + zend_string_free(func_name); + + PHP_EMBED_END_BLOCK() +} +``` + +## Execute a PHP script example + +```php + + +int main(int argc, char **argv) +{ + PHP_EMBED_START_BLOCK(argc, argv) + + zend_file_handle file_handle; + zend_stream_init_filename(&file_handle, "example.php"); + + if (php_execute_script(&file_handle) == FAILURE) { + php_printf("Failed to execute PHP script.\n"); + } + + PHP_EMBED_END_BLOCK() +} +``` + +## INI defaults + +The default value for 'error_prepend_string' is 'NULL'. The following example sets the INI default for 'error_prepend_string' to 'Embed SAPI error:'. + +```c +#include + +/* This callback is invoked as soon as the configuration hash table is + * allocated so any INI settings added via this callback will have the lowest + * precedence and will allow INI files to overwrite them. + */ +static void example_ini_defaults(HashTable *configuration_hash) +{ + zval ini_value; + ZVAL_NEW_STR(&ini_value, zend_string_init(ZEND_STRL("Embed SAPI error:"), /* persistent */ 1)); + zend_hash_str_update(configuration_hash, ZEND_STRL("error_prepend_string"), &ini_value); +} + +int main(int argc, char **argv) +{ + php_embed_module.ini_defaults = example_ini_defaults; + + PHP_EMBED_START_BLOCK(argc, argv) + + zval retval; + + /* Generates an error by accessing an undefined variable '$a'. */ + if (zend_eval_stringl(ZEND_STRL("var_dump($a);"), &retval, "example") == FAILURE) { + php_printf("Failed to eval PHP.\n"); + } + + PHP_EMBED_END_BLOCK() +} +``` + +After compiling and running, you should see: + +``` +Embed SAPI error: +Warning: Undefined variable $a in example on line 1 +NULL +``` + +This default value is overwritable from INI files. We'll update one of the INI files (which can be found by running `$ php --ini`), and set `error_prepend_string="Oops!"`. We don't have to recompile the program, we can just run it again and we should see: + +``` +Oops! +Warning: Undefined variable $a in example on line 1 +NULL +``` diff --git a/sapi/embed/php_embed.c b/sapi/embed/php_embed.c index b510d5105e5f6..dd4c85624e156 100644 --- a/sapi/embed/php_embed.c +++ b/sapi/embed/php_embed.c @@ -46,6 +46,12 @@ static int php_embed_deactivate(void) return SUCCESS; } +/* Here we prefer to use write(), which is unbuffered, over fwrite(), which is + * buffered. Using an unbuffered write operation to stdout will ensure PHP's + * output buffering feature does not compete with a SAPI output buffer and + * therefore we avoid situations wherein flushing the output buffer results in + * nondeterministic behavior. + */ static inline size_t php_embed_single_write(const char *str, size_t str_length) { #ifdef PHP_WRITE_STDOUT @@ -62,7 +68,10 @@ static inline size_t php_embed_single_write(const char *str, size_t str_length) #endif } - +/* SAPIs only have unbuffered write operations. This is because PHP's output + * buffering feature will handle any buffering of the output and invoke the + * SAPI unbuffered write operation when it flushes the buffer. + */ static size_t php_embed_ub_write(const char *str, size_t str_length) { const char *ptr = str; @@ -92,6 +101,11 @@ static void php_embed_send_header(sapi_header_struct *sapi_header, void *server_ { } +/* The SAPI error logger that is called when the 'error_log' INI setting is not + * set. + * + * https://www.php.net/manual/en/errorfunc.configuration.php#ini.error-log + */ static void php_embed_log_message(const char *message, int syslog_type_int) { fprintf(stderr, "%s\n", message); @@ -102,9 +116,10 @@ static void php_embed_register_variables(zval *track_vars_array) php_import_environment_variables(track_vars_array); } +/* Module initialization (MINIT) */ static int php_embed_startup(sapi_module_struct *sapi_module) { - if (php_module_startup(sapi_module, NULL, 0)==FAILURE) { + if (php_module_startup(sapi_module, NULL, 0) == FAILURE) { return FAILURE; } return SUCCESS; @@ -114,14 +129,14 @@ EMBED_SAPI_API sapi_module_struct php_embed_module = { "embed", /* name */ "PHP Embedded Library", /* pretty name */ - php_embed_startup, /* startup */ + php_embed_startup, /* startup */ php_module_shutdown_wrapper, /* shutdown */ NULL, /* activate */ - php_embed_deactivate, /* deactivate */ + php_embed_deactivate, /* deactivate */ - php_embed_ub_write, /* unbuffered write */ - php_embed_flush, /* flush */ + php_embed_ub_write, /* unbuffered write */ + php_embed_flush, /* flush */ NULL, /* get uid */ NULL, /* getenv */ @@ -129,15 +144,15 @@ EMBED_SAPI_API sapi_module_struct php_embed_module = { NULL, /* header handler */ NULL, /* send headers handler */ - php_embed_send_header, /* send header handler */ + php_embed_send_header, /* send header handler */ NULL, /* read POST data */ - php_embed_read_cookies, /* read Cookies */ + php_embed_read_cookies, /* read Cookies */ - php_embed_register_variables, /* register server variables */ - php_embed_log_message, /* Log message */ - NULL, /* Get request time */ - NULL, /* Child terminate */ + php_embed_register_variables, /* register server variables */ + php_embed_log_message, /* Log message */ + NULL, /* Get request time */ + NULL, /* Child terminate */ STANDARD_SAPI_MODULE_PROPERTIES }; @@ -150,8 +165,6 @@ static const zend_function_entry additional_functions[] = { EMBED_SAPI_API int php_embed_init(int argc, char **argv) { - zend_llist global_vars; - #if defined(SIGPIPE) && defined(SIG_IGN) signal(SIGPIPE, SIG_IGN); /* ignore SIGPIPE in standalone mode so that sockets created via fsockopen() @@ -162,63 +175,99 @@ EMBED_SAPI_API int php_embed_init(int argc, char **argv) #endif #ifdef ZTS - php_tsrm_startup(); + php_tsrm_startup(); # ifdef PHP_WIN32 - ZEND_TSRMLS_CACHE_UPDATE(); + ZEND_TSRMLS_CACHE_UPDATE(); # endif #endif zend_signal_startup(); - sapi_startup(&php_embed_module); + /* SAPI initialization (SINIT) + * + * Initialize the SAPI globals (memset to 0). After this point we can set + * SAPI globals via the SG() macro. + * + * Reentrancy startup. + * + * This also sets 'php_embed_module.ini_entries = NULL' so we cannot + * allocate the INI entries until after this call. + */ + sapi_startup(&php_embed_module); #ifdef PHP_WIN32 - _fmode = _O_BINARY; /*sets default for file streams to binary */ - setmode(_fileno(stdin), O_BINARY); /* make the stdio mode be binary */ - setmode(_fileno(stdout), O_BINARY); /* make the stdio mode be binary */ - setmode(_fileno(stderr), O_BINARY); /* make the stdio mode be binary */ + _fmode = _O_BINARY; /*sets default for file streams to binary */ + setmode(_fileno(stdin), O_BINARY); /* make the stdio mode be binary */ + setmode(_fileno(stdout), O_BINARY); /* make the stdio mode be binary */ + setmode(_fileno(stderr), O_BINARY); /* make the stdio mode be binary */ #endif - php_embed_module.ini_entries = malloc(sizeof(HARDCODED_INI)); - memcpy(php_embed_module.ini_entries, HARDCODED_INI, sizeof(HARDCODED_INI)); - - php_embed_module.additional_functions = additional_functions; - - if (argv) { - php_embed_module.executable_location = argv[0]; - } + /* This hard-coded string of INI settings is parsed and read into PHP's + * configuration hash table at the very end of php_init_config(). This + * means these settings will overwrite any INI settings that were set from + * an INI file. + * + * To provide overwritable INI defaults, hook the ini_defaults function + * pointer that is part of the sapi_module_struct + * (php_embed_module.ini_defaults). + * + * void (*ini_defaults)(HashTable *configuration_hash); + * + * This callback is invoked as soon as the configuration hash table is + * allocated so any INI settings added via this callback will have the + * lowest precedence and will allow INI files to overwrite them. + */ + php_embed_module.ini_entries = malloc(sizeof(HARDCODED_INI)); + memcpy(php_embed_module.ini_entries, HARDCODED_INI, sizeof(HARDCODED_INI)); + + /* SAPI-provided functions. */ + php_embed_module.additional_functions = additional_functions; + + if (argv) { + php_embed_module.executable_location = argv[0]; + } - if (php_embed_module.startup(&php_embed_module)==FAILURE) { - return FAILURE; - } + /* Module initialization (MINIT) */ + if (php_embed_module.startup(&php_embed_module) == FAILURE) { + return FAILURE; + } - zend_llist_init(&global_vars, sizeof(char *), NULL, 0); + /* Do not chdir to the script's directory. This is akin to calling the CGI + * SAPI with '-C'. + */ + SG(options) |= SAPI_OPTION_NO_CHDIR; - /* Set some Embedded PHP defaults */ - SG(options) |= SAPI_OPTION_NO_CHDIR; - SG(request_info).argc=argc; - SG(request_info).argv=argv; + SG(request_info).argc=argc; + SG(request_info).argv=argv; - if (php_request_startup()==FAILURE) { - php_module_shutdown(); - return FAILURE; - } + /* Request initialization (RINIT) */ + if (php_request_startup() == FAILURE) { + php_module_shutdown(); + return FAILURE; + } - SG(headers_sent) = 1; - SG(request_info).no_headers = 1; - php_register_variable("PHP_SELF", "-", NULL); + SG(headers_sent) = 1; + SG(request_info).no_headers = 1; + php_register_variable("PHP_SELF", "-", NULL); - return SUCCESS; + return SUCCESS; } EMBED_SAPI_API void php_embed_shutdown(void) { + /* Request shutdown (RSHUTDOWN) */ php_request_shutdown((void *) 0); + + /* Module shutdown (MSHUTDOWN) */ php_module_shutdown(); + + /* SAPI shutdown (SSHUTDOWN) */ sapi_shutdown(); + #ifdef ZTS - tsrm_shutdown(); + tsrm_shutdown(); #endif + if (php_embed_module.ini_entries) { free(php_embed_module.ini_entries); php_embed_module.ini_entries = NULL; From 76225c194f08acdd79043f3ba4078d64f26d7ed7 Mon Sep 17 00:00:00 2001 From: Sammy Kaye Powers Date: Mon, 12 Apr 2021 08:13:43 -0700 Subject: [PATCH 2/3] Apply suggestions from code review Co-authored-by: Nikita Popov --- sapi/embed/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sapi/embed/README.md b/sapi/embed/README.md index 6a1b15bb5f062..319da5551b941 100644 --- a/sapi/embed/README.md +++ b/sapi/embed/README.md @@ -1,6 +1,6 @@ # The embed SAPI -A server application programming interface (SAPI), is the entry point into the Zend Engine. The embed SAPI is a lightweight SAPI for calling into the Zend Engine from C or other languages that have C bindings. +A server application programming interface (SAPI) is the entry point into the Zend Engine. The embed SAPI is a lightweight SAPI for calling into the Zend Engine from C or other languages that have C bindings. ## Basic Example @@ -31,7 +31,7 @@ int main(int argc, char **argv) } ``` -To compile this, we must point the compiler to the PHP header files. The paths to the header files are listed from (`php-config --includes`) but the path to the SAPI header files are not included in that list by default so we must explicitly include `$(php-config --include-dir)/sapi`. +To compile this, we must point the compiler to the PHP header files. The paths to the header files are listed from `php-config --includes`, but the path to the SAPI header files are not included in that list by default so we must explicitly include `$(php-config --include-dir)/sapi`. We must also point the linker and the runtime loader to the `libphp.so` shared lib for linking PHP (`-lphp`) which is located at `$(php-config --prefix)/lib`. So the complete command to compile ends up being: @@ -81,7 +81,7 @@ int main(int argc, char **argv) php_var_dump(&retval, 1); } - zend_string_free(func_name); + zend_string_release(func_name); PHP_EMBED_END_BLOCK() } From b601cf22b6e04ea398bc02de3990faf04b5c0f7f Mon Sep 17 00:00:00 2001 From: Sammy Kaye Powers Date: Mon, 12 Apr 2021 11:24:19 -0400 Subject: [PATCH 3/3] Remove superfluous sapi/ include step --- sapi/embed/README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sapi/embed/README.md b/sapi/embed/README.md index 319da5551b941..c90ff354ab7d9 100644 --- a/sapi/embed/README.md +++ b/sapi/embed/README.md @@ -31,14 +31,13 @@ int main(int argc, char **argv) } ``` -To compile this, we must point the compiler to the PHP header files. The paths to the header files are listed from `php-config --includes`, but the path to the SAPI header files are not included in that list by default so we must explicitly include `$(php-config --include-dir)/sapi`. +To compile this, we must point the compiler to the PHP header files. The paths to the header files are listed from `php-config --includes`. We must also point the linker and the runtime loader to the `libphp.so` shared lib for linking PHP (`-lphp`) which is located at `$(php-config --prefix)/lib`. So the complete command to compile ends up being: ```bash $ gcc \ $(php-config --includes) \ - -I$(php-config --include-dir)/sapi \ -L$(php-config --prefix)/lib \ embed_sapi_basic_example.c \ -lphp \