diff --git a/.gitignore b/.gitignore index 9339f573..c8d043c4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,3 @@ -*.o -*.a -*.so *.txt -*.a.* -*.so.* build/ -.vscode/ \ No newline at end of file +.vscode/ diff --git a/Examples/extism/Makefile b/Examples/extism/Makefile new file mode 100644 index 00000000..118ef074 --- /dev/null +++ b/Examples/extism/Makefile @@ -0,0 +1,136 @@ +# +# Makefile template +# +# This is an example Makefile that can be used by anyone who is building +# his or her own PHP extensions using the PHP-CPP library. +# +# In the top part of this file we have included variables that can be +# altered to fit your configuration, near the bottom the instructions and +# dependencies for the compiler are defined. The deeper you get into this +# file, the less likely it is that you will have to change anything in it. +# + +# +# Name of your extension +# +# This is the name of your extension. Based on this extension name, the +# name of the library file (name.so) and the name of the config file (name.ini) +# are automatically generated +# + +NAME = extismcpp + + +# +# Php.ini directories +# +# In the past, PHP used a single php.ini configuration file. Today, most +# PHP installations use a conf.d directory that holds a set of config files, +# one for each extension. Use this variable to specify this directory. +# + +INI_DIR = /etc/php/8.2/cli/conf.d/ + + +# +# The extension dirs +# +# This is normally a directory like /usr/lib/php5/20121221 (based on the +# PHP version that you use. We make use of the command line 'php-config' +# instruction to find out what the extension directory is, you can override +# this with a different fixed directory +# + +EXTENSION_DIR = $(shell php-config --extension-dir) + + +# +# The name of the extension and the name of the .ini file +# +# These two variables are based on the name of the extension. We simply add +# a certain extension to them (.so or .ini) +# + +EXTENSION = ${NAME}.so +INI = ${NAME}.ini + + +# +# Compiler +# +# By default, the GNU C++ compiler is used. If you want to use a different +# compiler, you can change that here. You can change this for both the +# compiler (the program that turns the c++ files into object files) and for +# the linker (the program that links all object files into the single .so +# library file. By default, g++ (the GNU C++ compiler) is used for both. +# + +COMPILER = g++ +LINKER = g++ + + +# +# Compiler and linker flags +# +# This variable holds the flags that are passed to the compiler. By default, +# we include the -O2 flag. This flag tells the compiler to optimize the code, +# but it makes debugging more difficult. So if you're debugging your application, +# you probably want to remove this -O2 flag. At the same time, you can then +# add the -g flag to instruct the compiler to include debug information in +# the library (but this will make the final libphpcpp.so file much bigger, so +# you want to leave that flag out on production servers). +# +# If your extension depends on other libraries (and it does at least depend on +# one: the PHP-CPP library), you should update the LINKER_DEPENDENCIES variable +# with a list of all flags that should be passed to the linker. +# + +COMPILER_FLAGS = -Wall -c -O2 -std=c++11 -fpic -o +LINKER_FLAGS = -shared +LINKER_DEPENDENCIES = -lphpcpp -lextism + + +# +# Command to remove files, copy files and create directories. +# +# I've never encountered a *nix environment in which these commands do not work. +# So you can probably leave this as it is +# + +RM = rm -f +CP = cp -f +MKDIR = mkdir -p + + +# +# All source files are simply all *.cpp files found in the current directory +# +# A builtin Makefile macro is used to scan the current directory and find +# all source files. The object files are all compiled versions of the source +# file, with the .cpp extension being replaced by .o. +# + +SOURCES = $(wildcard *.cpp) +OBJECTS = $(SOURCES:%.cpp=%.o) + + +# +# From here the build instructions start +# + +all: ${OBJECTS} ${EXTENSION} + +${EXTENSION}: ${OBJECTS} + ${LINKER} ${LINKER_FLAGS} -o $@ ${OBJECTS} ${LINKER_DEPENDENCIES} + +${OBJECTS}: + ${COMPILER} ${COMPILER_FLAGS} $@ ${@:%.o=%.cpp} + +install: + ${CP} ${EXTENSION} ${EXTENSION_DIR} + ${CP} ${INI} ${INI_DIR} + +clean: + ${RM} ${EXTENSION} ${OBJECTS} + sudo ${RM} ${EXTENSION_DIR}/${EXTENSION} + sudo ${RM} ${INI_DIR}/${INI} diff --git a/Examples/extism/README.md b/Examples/extism/README.md new file mode 100644 index 00000000..23d7db51 --- /dev/null +++ b/Examples/extism/README.md @@ -0,0 +1,50 @@ +This is just a proof of concept C++ PHP module that loads and runs WASM using https://github.com/extism/extism + +as an alternative to https://github.com/extism/php-sdk , which has the following issues: + +1 - uses C bindings, not C++ + +2 - uses php's FFI interface, which itself is a php loadable module like this one, so its got more levels of indirection + +3 - PHP FFI is slower that C++ bindings. + + +This is just barely enough to proove it worked for me, and no more. + +Here some output: +``` +cd ~/PHP-CPP/Examples/extism +[buzz@qbi-buzz extism]$ make clean ; make ; sudo make install ; time php extismcpp.php +rm -f extismcpp.so extismcpp.o +sudo rm -f /usr/lib/php/20220829/extismcpp.so +sudo rm -f /etc/php/8.2/cli/conf.d//extismcpp.ini +g++ -Wall -c -O2 -std=c++11 -fpic -o extismcpp.o extismcpp.cpp +g++ -shared -o extismcpp.so extismcpp.o -lphpcpp -lextism +cp -f extismcpp.so /usr/lib/php/20220829 +cp -f extismcpp.ini /etc/php/8.2/cli/conf.d/ +ExtismClass::ExtismClass([no params]) +C++ reading wasm... +reading WASM with read_file +done WASM with read_file length:18258 +C++ reading wasm done. +calling extism_function_new with hello_world +calling extism_plugin_new with hello function +sending a string to count to count_vowels... +Hello from hello_world C++! +0x7f6583a7135b +hello_world:{"count": 6}[hello output done] +print_plugin_output:{"count": 6}[output done] +count_vowels plugin said:{"count": 6}12 +Freeing userdata +------------------------------ +LoadWASM count vowels result: {"count": 6} +------------------------------ +ExtismClass::__destruct +ExtismClass::~ExtismClass + +real 0m0.037s +user 0m0.029s +sys 0m0.008s +``` +Yup, so with this... PHP can instantiate extism using its C++ bindings, load a wasm, register a plugin and call a function. I leave it to u to make the interface sane, and not hardcoding a bunch of stuff. + diff --git a/Examples/extism/_extism.h b/Examples/extism/_extism.h new file mode 100644 index 00000000..3439bc85 --- /dev/null +++ b/Examples/extism/_extism.h @@ -0,0 +1,286 @@ +#pragma once + +#include +#include + +#define EXTISM_FUNCTION(N) extern void N(ExtismCurrentPlugin*, const ExtismVal*, ExtismSize, ExtismVal*, ExtismSize, void*) +#define EXTISM_GO_FUNCTION(N) extern void N(void*, ExtismVal*, ExtismSize, ExtismVal*, ExtismSize, uintptr_t) + +/** The return code from extism_plugin_call used to signal a successful call with no errors */ +#define EXTISM_SUCCESS 0 + +/** An alias for I64 to signify an Extism pointer */ +#define PTR I64 + + +/** + * An enumeration of all possible value types in WebAssembly. + */ +typedef enum { + /** + * Signed 32 bit integer. + */ + I32, + /** + * Signed 64 bit integer. + */ + I64, + /** + * Floating point 32 bit integer. + */ + F32, + /** + * Floating point 64 bit integer. + */ + F64, + /** + * A 128 bit number. + */ + V128, + /** + * A reference to a Wasm function. + */ + FuncRef, + /** + * A reference to opaque data in the Wasm instance. + */ + ExternRef, +} ExtismValType; + +/** + * A `CancelHandle` can be used to cancel a running plugin from another thread + */ +typedef struct ExtismCancelHandle ExtismCancelHandle; + +/** + * CurrentPlugin stores data that is available to the caller in PDK functions, this should + * only be accessed from inside a host function + */ +typedef struct ExtismCurrentPlugin ExtismCurrentPlugin; + +typedef struct ExtismFunction ExtismFunction; + +/** + * Plugin contains everything needed to execute a WASM function + */ +typedef struct ExtismPlugin ExtismPlugin; + +typedef uint64_t ExtismMemoryHandle; + +typedef uint64_t ExtismSize; + +/** + * A union type for host function argument/return values + */ +typedef union { + int32_t i32; + int64_t i64; + float f32; + double f64; +} ExtismValUnion; + +/** + * `ExtismVal` holds the type and value of a function argument/return + */ +typedef struct { + ExtismValType t; + ExtismValUnion v; +} ExtismVal; + +/** + * Host function signature + */ +typedef void (*ExtismFunctionType)(ExtismCurrentPlugin *plugin, + const ExtismVal *inputs, + ExtismSize n_inputs, + ExtismVal *outputs, + ExtismSize n_outputs, + void *data); + +/** + * Log drain callback + */ +typedef void (*ExtismLogDrainFunctionType)(const char *data, ExtismSize size); + + + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +/** + * Get a plugin's ID, the returned bytes are a 16 byte buffer that represent a UUIDv4 + */ +const uint8_t *extism_plugin_id(ExtismPlugin *plugin); + +/** + * Returns a pointer to the memory of the currently running plugin + * NOTE: this should only be called from host functions. + */ +uint8_t *extism_current_plugin_memory(ExtismCurrentPlugin *plugin); + +/** + * Allocate a memory block in the currently running plugin + * NOTE: this should only be called from host functions. + */ +ExtismMemoryHandle extism_current_plugin_memory_alloc(ExtismCurrentPlugin *plugin, ExtismSize n); + +/** + * Get the length of an allocated block + * NOTE: this should only be called from host functions. + */ +ExtismSize extism_current_plugin_memory_length(ExtismCurrentPlugin *plugin, ExtismMemoryHandle n); + +/** + * Free an allocated memory block + * NOTE: this should only be called from host functions. + */ +void extism_current_plugin_memory_free(ExtismCurrentPlugin *plugin, ExtismMemoryHandle ptr); + +/** + * Create a new host function + * + * Arguments + * - `name`: function name, this should be valid UTF-8 + * - `inputs`: argument types + * - `n_inputs`: number of argument types + * - `outputs`: return types + * - `n_outputs`: number of return types + * - `func`: the function to call + * - `user_data`: a pointer that will be passed to the function when it's called + * this value should live as long as the function exists + * - `free_user_data`: a callback to release the `user_data` value when the resulting + * `ExtismFunction` is freed. + * + * Returns a new `ExtismFunction` or `null` if the `name` argument is invalid. + */ +ExtismFunction *extism_function_new(const char *name, + const ExtismValType *inputs, + ExtismSize n_inputs, + const ExtismValType *outputs, + ExtismSize n_outputs, + ExtismFunctionType func, + void *user_data, + void (*free_user_data)(void *_)); + +/** + * Free `ExtismFunction` + */ +void extism_function_free(ExtismFunction *f); + +/** + * Set the namespace of an `ExtismFunction` + */ +void extism_function_set_namespace(ExtismFunction *ptr, const char *namespace_); + +/** + * Create a new plugin with host functions, the functions passed to this function no longer need to be manually freed using + * + * `wasm`: is a WASM module (wat or wasm) or a JSON encoded manifest + * `wasm_size`: the length of the `wasm` parameter + * `functions`: an array of `ExtismFunction*` + * `n_functions`: the number of functions provided + * `with_wasi`: enables/disables WASI + */ +ExtismPlugin *extism_plugin_new(const uint8_t *wasm, + ExtismSize wasm_size, + const ExtismFunction **functions, + ExtismSize n_functions, + bool with_wasi, + char **errmsg); + +/** + * Free the error returned by `extism_plugin_new`, errors returned from `extism_plugin_error` don't need to be freed + */ +void extism_plugin_new_error_free(char *err); + +/** + * Remove a plugin from the registry and free associated memory + */ +void extism_plugin_free(ExtismPlugin *plugin); + +/** + * Get handle for plugin cancellation + */ +const ExtismCancelHandle *extism_plugin_cancel_handle(const ExtismPlugin *plugin); + +/** + * Cancel a running plugin + */ +bool extism_plugin_cancel(const ExtismCancelHandle *handle); + +/** + * Update plugin config values. + */ +bool extism_plugin_config(ExtismPlugin *plugin, const uint8_t *json, ExtismSize json_size); + +/** + * Returns true if `func_name` exists + */ +bool extism_plugin_function_exists(ExtismPlugin *plugin, const char *func_name); + +/** + * Call a function + * + * `func_name`: is the function to call + * `data`: is the input data + * `data_len`: is the length of `data` + */ +int32_t extism_plugin_call(ExtismPlugin *plugin, + const char *func_name, + const uint8_t *data, + ExtismSize data_len); + +/** + * Get the error associated with a `Plugin` + */ +const char *extism_error(ExtismPlugin *plugin); + +/** + * Get the error associated with a `Plugin` + */ +const char *extism_plugin_error(ExtismPlugin *plugin); + +/** + * Get the length of a plugin's output data + */ +ExtismSize extism_plugin_output_length(ExtismPlugin *plugin); + +/** + * Get a pointer to the output data + */ +const uint8_t *extism_plugin_output_data(ExtismPlugin *plugin); + +/** + * Set log file and level. + * The log level can be either one of: info, error, trace, debug, warn or a more + * complex filter like `extism=trace,cranelift=debug` + * The file will be created if it doesn't exist. + */ +bool extism_log_file(const char *filename, const char *log_level); + +/** + * Enable a custom log handler, this will buffer logs until `extism_log_drain` is called + * Log level should be one of: info, error, trace, debug, warn + */ +bool extism_log_custom(const char *log_level); + +/** + * Calls the provided callback function for each buffered log line. + * This is only needed when `extism_log_custom` is used. + */ +void extism_log_drain(ExtismLogDrainFunctionType handler); + +/** + * Reset the Extism runtime, this will invalidate all allocated memory + */ +bool extism_plugin_reset(ExtismPlugin *plugin); + +/** + * Get the Extism version string + */ +const char *extism_version(void); + +#ifdef __cplusplus +} // extern "C" +#endif // __cplusplus diff --git a/Examples/extism/code-functions.wasm b/Examples/extism/code-functions.wasm new file mode 100644 index 00000000..d13506de Binary files /dev/null and b/Examples/extism/code-functions.wasm differ diff --git a/Examples/extism/extismcpp.cpp b/Examples/extism/extismcpp.cpp new file mode 100644 index 00000000..b88e52fa --- /dev/null +++ b/Examples/extism/extismcpp.cpp @@ -0,0 +1,284 @@ +/** + * extismcpp.cpp + * @author Jasper van Eck + * + * An example file to show the working of using a C++ class in PHP. + */ + +#include +#include + +/** + * Our own library/s + */ +#include // /usr/include/phpcpp.h +#include // /usr/local/include/extism.h + +#include +#include + +#include + +#include + +/** + * Namespace to use + */ +using namespace std; + +Php::Value my_no_parameters_function() +{ + return "42"; +} + +/** + * This functions receives a reference to a variable. When the variable is altered, + * so is the value in the php script. + * my_with_defined_parameters_reference_function() + * @param Php::Parameters the given parameters + */ +Php::Value my_with_defined_parameters_reference_function(Php::Parameters ¶ms) +{ + // params[0] = "I changed!"; + // return params[0]; + + return "stuff"; +} + +// a few functions used by extism that are very lightly ported to C++ from C, just enough to compile. + +uint8_t *read_file(const char *filename, size_t *len) { + + std::cout << "reading WASM with read_file" << std::endl; + + FILE *fp = fopen(filename, "rb"); + if (fp == NULL) { + return NULL; + } + fseek(fp, 0, SEEK_END); + size_t length = ftell(fp); + fseek(fp, 0, SEEK_SET); + + uint8_t *data = (uint8_t*)malloc(length); + if (data == NULL) { + fclose(fp); + return NULL; + } + + assert(fread(data, 1, length, fp) == length); + fclose(fp); + + *len = length; + + std::cout << "done WASM with read_file length:" << length << std::endl; + + return data; +} + +void hello_world(ExtismCurrentPlugin *plugin, const ExtismVal *inputs, + uint64_t n_inputs, ExtismVal *outputs, uint64_t n_outputs, + void *data) { + std::cout << "Hello from hello_world C++!" << std::endl; + + std::cout << data << std::endl; + + ExtismSize ptr_offs = inputs[0].v.i64; + + uint8_t *buf = extism_current_plugin_memory(plugin) + ptr_offs; + //uint64_t length = extism_current_plugin_memory_length(plugin, ptr_offs); + //fwrite(buf, length, 1, stdout); + //fputc('\n', stdout); + + std::cout << "hello_world:" << buf << "[hello output done]" << std::endl; + + outputs[0].v.i64 = inputs[0].v.i64; +} + +void log_handler(const char *line, uintptr_t length) { + fwrite(line, length, 1, stderr); +} + +void print_plugin_output(ExtismPlugin *plugin, int32_t rc){ + if (rc != EXTISM_SUCCESS) { + //fprintf(stderr, "ERROR: %s\n", extism_plugin_error(plugin)); + std::cout << "ERR" << extism_plugin_error(plugin) << std::endl; + return; + } + + //size_t outlen = extism_plugin_output_length(plugin); + const uint8_t *out = extism_plugin_output_data(plugin); + //write(STDOUT_FILENO, out, outlen); + std::cout << "print_plugin_output:" << out << "[output done]" << std::endl; +} + +void free_data(void *x) { puts("Freeing userdata"); } + + +class ExtismClass : public Php::Base // , public Php::Countable +{ +private: + int _x = -1; + +public: + ExtismClass() + { + std::cout << "ExtismClass::ExtismClass([no params])" << std::endl; + } + + ExtismClass(int value) : _x(value) + { + std::cout << "ExtismClass::ExtismClass(" << value << ")" << std::endl; + } + + ExtismClass(const ExtismClass &that) + { + std::cout << "ExtismClass::ExtismClass copy constructor" << std::endl; + } + + virtual ~ExtismClass() + { + std::cout << "ExtismClass::~ExtismClass" << std::endl; + } + + virtual void __construct() + { + std::cout << "ExtismClass::__construct" << std::endl; + } + + virtual void __destruct() + { + std::cout << "ExtismClass::__destruct" << std::endl; + } + + virtual Php::Value count() //override + { + return _x; + } + + Php::Value handlestring(Php::Parameters ¶ms) //override + { + if (params.size() != 1) throw Php::Exception("Invalid number of parameters supplied"); + + string str = params[0].rawValue(); // the first param is a string. .rawValue() forces the Php::Value to give us a 'const char *' + + std::cout << "C++ handling string:" << str << std::endl; + return str; + } + + Php::Value LoadWASM(Php::Parameters ¶ms) + { + if (params.size() != 1) throw Php::Exception("Invalid number of parameters supplied"); + + // params[0] = the wasm filename + + // the result string is returned + + string filename = params[0].rawValue(); // the first param is a string. .rawValue() forces the Php::Value to give us a 'const char *' + + // todo use c++ isms to load it here rather than like this... this just proves we can load it really. + std::cout << "C++ reading wasm..." << std::endl; + + size_t len = 0; + uint8_t *data = read_file(filename.c_str(), &len); + ExtismValType inputs[] = {PTR}; + ExtismValType outputs[] = {PTR}; + char *errmsg = NULL; + + std::cout << "C++ reading wasm done." << std::endl; + + std::cout << "calling extism_function_new with hello_world" << std::endl; + + ExtismFunction *f = extism_function_new("hello_world", inputs, 1, outputs, 1, hello_world, (void*) "Hello, again!", free_data); + + std::cout << "calling extism_plugin_new with hello function" << std::endl; + + ExtismPlugin *plugin = extism_plugin_new(data, len, (const ExtismFunction **)&f, 1, true, &errmsg); + free(data); + if (plugin == NULL) { + puts(errmsg); + extism_plugin_new_error_free(errmsg); + exit(1); + } + + std::cout << "sending a string to count to count_vowels..." << std::endl; + const char *input = "Hello, count_vowels!"; + print_plugin_output(plugin, extism_plugin_call(plugin, "count_vowels", + (const uint8_t *)input, strlen(input))); + + ExtismSize out_len = extism_plugin_output_length(plugin); + const uint8_t *output = extism_plugin_output_data(plugin); + //write(STDOUT_FILENO, output, out_len); + std::cout << "count_vowels plugin said:" << output << out_len << std::endl; + //write(STDOUT_FILENO, "\n", 1); + + std::string myString; + myString.resize(out_len); + memcpy(&myString[0], &output[0], out_len); + + extism_plugin_free(plugin); + extism_function_free(f); + extism_log_drain(log_handler); + + return myString; + } + + Php::Value Factory(Php::Parameters ¶ms) + { + + // check number of parameters + if (params.size() != 1) throw Php::Exception("Invalid number of parameters supplied"); + + std::cout << "Factory called from object " << _x << std::endl; + std::cout << "Factory returning new object " << params[0] << std::endl; + + return Php::Object("ExtismClass", new ExtismClass(params[0])); + + } +}; + +// Symbols are exported according to the "C" language +extern "C" +{ + // export the "get_module" function that will be called by the Zend engine + PHPCPP_EXPORT void *get_module() + { + // create extension + static Php::Extension extension("extismcpp","1.0"); + + // add function to extension + extension.add("my_no_parameters_function"); + + // add function, with defined parameter by reference, to extension + extension.add("my_with_defined_parameters_reference_function", { + Php::ByRef("string", Php::Type::String) + }); + + + // // we are going to define a class + Php::Class customClass("ExtismClass"); + + // // add methods to it + customClass.method<&ExtismClass::Factory>("Factory", Php::Final, {}); + + customClass.method<&ExtismClass::LoadWASM>("LoadWASM",Php::Final, { + Php::ByVal("string", Php::Type::String)//, // filename in + //Php::ByRef("string", Php::Type::String) // result out by reference + }); + + // customClass.method<&ExtismClass::count>("count"); + + // customClass.method<&ExtismClass::handlestring>("handlestring"); + // { + // Php::ByVal("change", Php::Type::String, true); // true means its a required param, and a string. + // } + + // customClass.property("property1", "prop1"); + // customClass.property("property2", "prop2"); + + // add the class to the extension + extension.add(customClass); + + // return the extension module + return extension; + } +} diff --git a/Examples/extism/extismcpp.ini b/Examples/extism/extismcpp.ini new file mode 100644 index 00000000..38189528 --- /dev/null +++ b/Examples/extism/extismcpp.ini @@ -0,0 +1,4 @@ +; configuration for phpcpp module +; priority=30 +extension=extismcpp.so + diff --git a/Examples/extism/extismcpp.o b/Examples/extism/extismcpp.o new file mode 100644 index 00000000..2e604ca2 Binary files /dev/null and b/Examples/extism/extismcpp.o differ diff --git a/Examples/extism/extismcpp.php b/Examples/extism/extismcpp.php new file mode 100644 index 00000000..1e3f4197 --- /dev/null +++ b/Examples/extism/extismcpp.php @@ -0,0 +1,22 @@ + + * + * inspired by some of the PHP-CPP examples as good reference material + */ + + + +//create a ExtismClass object, which is an object of a C++ class +$extismInstance = new ExtismClass(); // when called with no params , it inits with -1 + + +//$another_extism_Instance = $extismInstance->Factory(1); // object with the default value -1 created above, instantiates a new one defaulting to 1. + + +$result = $extismInstance->LoadWASM("./code-functions.wasm"); +echo "------------------------------\n"; +echo("LoadWASM count vowels result: ".$result."\n"); +echo "------------------------------\n"; + diff --git a/Examples/extism/extismcpp.so b/Examples/extism/extismcpp.so new file mode 100755 index 00000000..fc43fb59 Binary files /dev/null and b/Examples/extism/extismcpp.so differ diff --git a/Examples/extism/libextism.so b/Examples/extism/libextism.so new file mode 100644 index 00000000..307086b9 Binary files /dev/null and b/Examples/extism/libextism.so differ diff --git a/libphpcpp.a.2.4.2 b/libphpcpp.a.2.4.2 new file mode 100644 index 00000000..48188444 Binary files /dev/null and b/libphpcpp.a.2.4.2 differ diff --git a/libphpcpp.so.2.4.2 b/libphpcpp.so.2.4.2 new file mode 100755 index 00000000..02420dfd Binary files /dev/null and b/libphpcpp.so.2.4.2 differ