Skip to content

Add some documentation to the embed SAPI #6856

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 166 additions & 0 deletions sapi/embed/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
# 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 <sapi/embed/php_embed.h>

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`.

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) \
-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 <main/php.h>
#include <ext/standard/php_var.h>
#include <sapi/embed/php_embed.h>

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_release(func_name);

PHP_EMBED_END_BLOCK()
}
```

## Execute a PHP script example

```php
<?php

# example.php

echo 'Hello from userland!' . PHP_EOL;
```

```c
#include <sapi/embed/php_embed.h>

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 <sapi/embed/php_embed.h>

/* 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
```
141 changes: 95 additions & 46 deletions sapi/embed/php_embed.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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;
Expand All @@ -114,30 +129,30 @@ 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 */

php_error, /* error handler */

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
};
Expand All @@ -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()
Expand All @@ -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;
Expand Down