diff options
author | Emiel Bruijntjes <emiel.bruijntjes@copernica.com> | 2015-01-10 21:13:13 +0100 |
---|---|---|
committer | Emiel Bruijntjes <emiel.bruijntjes@copernica.com> | 2015-01-10 21:13:13 +0100 |
commit | 8edf958a8ddf6c13b8bba9b87f3d5b1296be1376 (patch) | |
tree | a3fdeeb1b82db662df95f3fbfc14e2e6f8e199d2 | |
parent | fb80367d418f9a9fb059c98d5aec80febeb3bd23 (diff) |
added Script class to simplify parsing and executing php scripts (the Php::eval() call both compiles and executes a script, while the Script class splits these two steps, which allows you to run the same opcodes multiple times)
-rw-r--r-- | include/script.h | 95 | ||||
-rw-r--r-- | include/value.h | 1 | ||||
-rw-r--r-- | phpcpp.h | 1 | ||||
-rw-r--r-- | zend/includes.h | 1 | ||||
-rw-r--r-- | zend/script.cpp | 199 |
5 files changed, 297 insertions, 0 deletions
diff --git a/include/script.h b/include/script.h new file mode 100644 index 0000000..d2eb575 --- /dev/null +++ b/include/script.h @@ -0,0 +1,95 @@ +/** + * Script.h + * + * Class that can be used to evaluate a PHP script in the current PHP context. + * + * The difference between directly calling eval() is that the script object + * will first evaluate the string, and then it can be executed multiple times. + * + * @author Emiel Bruijntjes <emiel.bruijntjes@copernica.com> + * @copyright 2014 Copernica BV + */ + +/** + * Forward declarations + */ +struct _zend_op_array; + +/** + * Set up namespace + */ +namespace Php { + +/** + * Class definition + */ +class Script +{ +public: + /** + * Constructor + * + * The constructor will not throw any exceptions, even when invalid + * PHP code is passed to it that can not be evaluated. You should call + * the valid() to find out if the script was valid (could be parsed). + * + * @param name Name of the PHP script + * @param source PHP source code to be evaluated + * @param size Length of the source code + */ + Script(const char *name, const char *source, size_t size) noexcept; + + /** + * Alternative constructor without a size + * @param name Name of the PHP script + * @param source PHP source code to be evaluated + */ + Script(const char *name, const char *source) noexcept : Script(name, source, ::strlen(source)) {} + + /** + * Alternative constructor without a name + * @param source PHP source code to be evaluated + * @param size Length of the source code + */ + Script(const char *source, size_t size) noexcept : Script("Unknown", source, size) {} + + /** + * Alternative constructor without a name and without a size + * @param source PHP source code to be evaluated + */ + Script(const char *source) noexcept : Script("Unknown", source, ::strlen(source)) {} + + /** + * Destructor + */ + virtual ~Script(); + + /** + * Is the script a valid PHP script without syntax errors? + * @return bool + */ + bool valid() const + { + return _opcodes != nullptr; + } + + /** + * Execute the script + * The return value of the script is returned + * @return Value + */ + Value execute() const; + +private: + /** + * The opcodes + * @var zend_op_array + */ + struct _zend_op_array *_opcodes; + +}; + +/** + * End of namespace + */ +} diff --git a/include/value.h b/include/value.h index f0b7345..9447fc8 100644 --- a/include/value.h +++ b/include/value.h @@ -1170,6 +1170,7 @@ protected: friend class HashMember<int>; friend class HashMember<std::string>; friend class Callable; + friend class Script; }; /** @@ -61,6 +61,7 @@ #include <phpcpp/namespace.h> #include <phpcpp/extension.h> #include <phpcpp/call.h> +#include <phpcpp/script.h> /** * Macro to export a function diff --git a/zend/includes.h b/zend/includes.h index 582530b..2f8990d 100644 --- a/zend/includes.h +++ b/zend/includes.h @@ -80,6 +80,7 @@ #include "../include/namespace.h" #include "../include/extension.h" #include "../include/call.h" +#include "../include/script.h" /** * Common header files for internal use only diff --git a/zend/script.cpp b/zend/script.cpp new file mode 100644 index 0000000..6cebaeb --- /dev/null +++ b/zend/script.cpp @@ -0,0 +1,199 @@ +/** + * Script.cpp + * + * Implementation file for the script class + * + * @author Emiel Bruijntjes <emiel.bruijntjes@copernica.com> + * @copyright 2014 Copernica BV + */ + +/** + * Dependencies + */ +#include "includes.h" +#include <mutex> + +/** + * Open PHP namespace + */ +namespace Php { + +/** + * Helper class to temporarily set compiler options + * + * When an object is destructed, it automatically restored the previous compiler settings + */ +class CompilerOptions +{ +private: + /** + * The original compiler options + * @var int + */ + zend_uint _original; + +public: + /** + * Constructor + * @param options + */ + CompilerOptions(zend_uint options) + { + // remember the old compiler options before we set temporary compile options + _original = CG(compiler_options); + + // we're going to evaluate only once + CG(compiler_options) = options; + } + + /** + * Destructor + */ + virtual ~CompilerOptions() + { + // restore original options + CG(compiler_options) = _original; + } +}; + +/** + * Helper class to store and restore the current opcode state + * + * When we're going to execute a set of instructions, we need to store the + * current state of the Zend engine. After the instructions have been processed, + * we can switch back to the original instructions + */ +class ExecuteState +{ +private: + /** + * All the original settings + */ + zend_op_array *_active_op_array; + zval **_return_value_ptr_ptr; + zend_op **_opline_ptr; + int _interactive; + +public: + /** + * Constructor + */ + ExecuteState() + { + // store all the original stuff + _active_op_array = EG(active_op_array); + _return_value_ptr_ptr = EG(return_value_ptr_ptr); + _opline_ptr = EG(opline_ptr); + _interactive = CG(interactive); + } + + /** + * Destructor + */ + virtual ~ExecuteState() + { + // restore all settings + CG(interactive) = _interactive; + EG(no_extensions) = 0; + EG(opline_ptr) = _opline_ptr; + EG(active_op_array) = _active_op_array; + EG(return_value_ptr_ptr) = _return_value_ptr_ptr; + } +}; + +/** + * Constructor + * @param name name of the script + * @param script actual PHP code + * @param size length of the string + */ +Script::Script(const char *name, const char *phpcode, size_t size) +{ + // Sadly, there is not a simple Zend function to compile a string into opcodes, + // so we basically copy the code that we found in zend_execute_API.c inside + // the zend_eval_stringl() function into this file here. However, the code + // found there is full of zval manipulation, for which we can use the much + // simpler Php::Value object + Php::Value source(phpcode, size); + + // remember the old compiler options, and set new compiler options + CompilerOptions options(ZEND_COMPILE_DEFAULT_FOR_EVAL); + + // we need the tsrm_ls variable + TSRMLS_FETCH(); + + // compile the string + _opcodes = zend_compile_string(source._val, (char *)name TSRMLS_CC); +} + +/** + * Destructor + */ +Script::~Script() +{ + // do nothing if there are no opcodes + if (!_opcodes) return; + + // clean up opcodes + destroy_op_array(_opcodes TSRMLS_CC); + efree(_opcodes); +} + +/** + * Execute a script + * @return Value + */ +Value Script::execute() const +{ + // if the script could not be compiled, we return null + if (!_opcodes) return nullptr; + + // pointer that is going to hold the return value of the script + zval *retval_ptr = nullptr; + + // the zend engine is probably already busy processing opcodes, so we store + // the current execute state before we're going to switch the runtime to + // our own set of opcodes + ExecuteState oldstate; + + // old execute state has been saved (and will automatically be restured when + // the oldstate is destructed), so we can now safely overwrite all the settings + EG(return_value_ptr_ptr) = &retval_ptr; + EG(active_op_array) = _opcodes; + EG(no_extensions) = 1; + if (!EG(active_symbol_table)) zend_rebuild_symbol_table(TSRMLS_C); + CG(interactive) = 0; + + // this code was copied from zend_execute_API.c. this is what I think it does: + // the zend_execute() call could result in all sort of disastrous things, one + // of them is a full crash of the executing script. if that happens, the zend + // engine does a long jump right up to some other point in the code. the + // 'zend_try' and 'zend_catch' macros prevent this: they install a new + // destination for the long jump, so that we can catch a failure + zend_try + { + // execute the code + zend_execute(_opcodes TSRMLS_CC); + } + zend_catch + { + // the code could not be executed, and the zend engine is in big + // trouble now, in the original code the _opcodes are efree'd, but here + // we can just as well continue with the real bailout (the zend_try/ + // zend_catch pair was maybe not even necessary???) + zend_bailout(); + } + zend_end_try(); + + // we're ready if there is no return value + if (!retval_ptr) return nullptr; + + // copy the pointer into a value object, and return that + return retval_ptr; +} + +/** + * End of namespace + */ +} + |