|
| 1 | ++++ |
| 2 | +draft = false |
| 3 | +title = 'Hello Swift' |
| 4 | ++++ |
| 5 | + |
| 6 | +SwiftPHP is an effort to make PHP extensions in a safer language than C. Every PHP minor release contains memory leaks and buffer overflow which is easy to create in C, but harder to do the same in Swift. Swift also has great concurrency constructs, allowing you to access more compute in your native extensions. |
| 7 | + |
| 8 | +I have chosen to make the API close to the C API for the first release so anyone coming from the C API or wants to use the C API for documentation can. It also makes migrating existing C extensions easier. |
| 9 | + |
| 10 | +Why not "X-language"? Well, PHP's C-API is mostly C-Macros which is not possible to replicate in any other language other than C++. |
| 11 | + |
| 12 | + - **Zig** - I have created a prototype to replicate many of the C-Macros however its too agressive in its C-Interop that its cause so much work and custom PHP C Core patches than its worth. Zig is also volitile in its API |
| 13 | + - **Rust** - Someone already did this work in Rust https://github.com/davidcole1340/ext-php-rs |
| 14 | + |
| 15 | +## Supported PHP Versions |
| 16 | + |
| 17 | + - PHP 8.2 (Unsupported) |
| 18 | + - PHP 8.3 (Unsupported) |
| 19 | + - PHP 8.4 Thread-safe (ZTS) |
| 20 | + - PHP 8.5 (Pending Testing) |
| 21 | + |
| 22 | +## Supported Operating Systems |
| 23 | + |
| 24 | + - MacOS ARM64 |
| 25 | + - Windows 11 x64 and ARM64 |
| 26 | + - Windows 11 ARM64 |
| 27 | + - Linux x64 |
| 28 | + - Linux ARM64 |
| 29 | + |
| 30 | +## Hello World Skeleton |
| 31 | + |
| 32 | +```bash |
| 33 | +# Compile extension |
| 34 | +swift build -v --product SwiftPHPExtension |
| 35 | + |
| 36 | +# Run PHP with custom extension |
| 37 | +php -dextension=.build/arm64-apple-macosx/debug/libSwiftPHPExtension.dylib \ |
| 38 | + -r 'var_dump(confirm_myext_compiled(), myext_hello());' |
| 39 | + |
| 40 | +# Output: |
| 41 | +# string(68) "Congratulations! You have successfully compiled the Swift extension." |
| 42 | +# string(0) "" |
| 43 | +``` |
| 44 | + |
| 45 | +### PHP function: |
| 46 | +```php |
| 47 | +function myext_hello(?string $str): string |
| 48 | +{ |
| 49 | + if ($str === null) { |
| 50 | + return ""; |
| 51 | + } |
| 52 | + |
| 53 | + return "Hello " . $str; |
| 54 | +} |
| 55 | +``` |
| 56 | + |
| 57 | +### Native PHP Extension version in Swift |
| 58 | +```swift |
| 59 | +import PHPCore |
| 60 | +import Foundation |
| 61 | + |
| 62 | +// PHP function argument register for type checking |
| 63 | +@MainActor |
| 64 | +public let arginfo_myext_hello: [zend_internal_arg_info] = |
| 65 | + ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX( |
| 66 | + name: "myext_hello", |
| 67 | + return_reference: false, |
| 68 | + required_num_args: 0, // All parameters are optional |
| 69 | + type: UInt32(IS_STRING), |
| 70 | + allow_null: false |
| 71 | + ) |
| 72 | + + [ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE( |
| 73 | + pass_by_ref: false, |
| 74 | + name: "str", |
| 75 | + type_hint: UInt32(IS_STRING), |
| 76 | + allow_null: true, |
| 77 | + default_value: "\"\"")] |
| 78 | + |
| 79 | +// Your Swift function to register |
| 80 | +@_cdecl("zif_myext_hello") |
| 81 | +public func zif_myext_hello( |
| 82 | + execute_data: UnsafeMutablePointer<zend_execute_data>?, |
| 83 | + return_value: UnsafeMutablePointer<zval>?) { |
| 84 | + // Ensure return value is initialized (redundent but needed) |
| 85 | + guard let return_value: UnsafeMutablePointer<zval> = return_value else { |
| 86 | + return |
| 87 | + } |
| 88 | + |
| 89 | + // Safely do parameter capture |
| 90 | + var var_str: UnsafeMutablePointer<CChar>? = nil |
| 91 | + var var_len: Int = 0 |
| 92 | + do { |
| 93 | + // Start parameter parsing |
| 94 | + guard var state: ParseState = ZEND_PARSE_PARAMETERS_START( |
| 95 | + min: 0, max: 1, execute_data: execute_data |
| 96 | + ) else { |
| 97 | + return |
| 98 | + } |
| 99 | + |
| 100 | + // Any parameter parsed after this is optional |
| 101 | + Z_PARAM_OPTIONAL(state: &state) |
| 102 | + |
| 103 | + // If this was not optional Z_PARAM_STRING |
| 104 | + // would be the correct call instead. |
| 105 | + try Z_PARAM_STRING_OR_NULL( |
| 106 | + state: &state, dest: &var_str, destLen: &var_len |
| 107 | + ) |
| 108 | + |
| 109 | + try ZEND_PARSE_PARAMETERS_END(state: state) |
| 110 | + } catch { |
| 111 | + return |
| 112 | + } |
| 113 | + |
| 114 | + let swiftString: String |
| 115 | + if let cString = var_str { |
| 116 | + // A string (even an empty one) was passed, so we use it. |
| 117 | + swiftString = String(cString: cString) |
| 118 | + } else { |
| 119 | + // A `null` was passed or the argument was omitted. Return an empty string |
| 120 | + RETURN_STR(ZSTR_EMPTY_ALLOC(), return_value) |
| 121 | + return |
| 122 | + } |
| 123 | + |
| 124 | + // Format Swift String |
| 125 | + let message: String = "Hello \(swiftString)" |
| 126 | + |
| 127 | + // Convert back to PHP String |
| 128 | + let retval: UnsafeMutablePointer<zend_string>? = message.withCString { |
| 129 | + return zend_string_init(messagePtr, message.utf8.count, false) |
| 130 | + } |
| 131 | + |
| 132 | + // Return the PHP String |
| 133 | + if let resultString: UnsafeMutablePointer<zend_string> = retval { |
| 134 | + RETURN_STR(resultString, return_value) |
| 135 | + } |
| 136 | +} |
| 137 | + |
| 138 | +// Global pointers to hold data that will persist |
| 139 | +@MainActor var myext_functions_ptr: UnsafeMutablePointer<zend_function_entry>? = nil |
| 140 | +@MainActor var myext_ini_entries_ptr: UnsafeMutablePointer<zend_ini_entry>? = nil |
| 141 | +@MainActor var myext_deps_ptr: UnsafeMutablePointer<zend_module_dep>? = nil |
| 142 | +@MainActor var myextModule_ptr: UnsafeMutablePointer<zend_module_entry>? = nil |
| 143 | +@MainActor var myext_globals_id: ts_rsrc_id = 0 |
| 144 | + |
| 145 | +struct myextGlobals { |
| 146 | + var someGlobalVariable: Int = 0 |
| 147 | +} |
| 148 | + |
| 149 | +@_cdecl("zm_startup_myext") |
| 150 | +func zm_startup_myext(type: Int32, module_number: Int32) -> Int32 { |
| 151 | + return Int32(SUCCESS.rawValue) |
| 152 | +} |
| 153 | + |
| 154 | +@_cdecl("zm_shutdown_myext") |
| 155 | +func zm_shutdown_myext(type: Int32, module_number: Int32) -> Int32 { |
| 156 | + return Int32(SUCCESS.rawValue) |
| 157 | +} |
| 158 | + |
| 159 | +@_cdecl("zm_activate_myext") |
| 160 | +func zm_activate_myext(type: Int32, module_number: Int32) -> Int32 { |
| 161 | + return Int32(SUCCESS.rawValue) |
| 162 | +} |
| 163 | + |
| 164 | +@_cdecl("zm_deactivate_myext") |
| 165 | +func zm_deactivate_myext(type: Int32, module_number: Int32) -> Int32 { |
| 166 | + return Int32(SUCCESS.rawValue) |
| 167 | +} |
| 168 | + |
| 169 | +@_cdecl("zm_info_myext") |
| 170 | +func zm_info_myext(zend_module: UnsafeMutableRawPointer?) { |
| 171 | + print("Myext Module Version: 2.0.0") |
| 172 | +} |
| 173 | + |
| 174 | +@_cdecl("zm_globals_ctor_myext") |
| 175 | +func zm_globals_ctor_myext(pointer: UnsafeMutableRawPointer?) { |
| 176 | + let globals = pointer!.bindMemory(to: myextGlobals.self, capacity: 1) |
| 177 | + globals.pointee.someGlobalVariable = 42 |
| 178 | +} |
| 179 | + |
| 180 | +@_cdecl("zm_globals_dtor_myext") |
| 181 | +func zm_globals_dtor_myext(pointer: UnsafeMutableRawPointer?) { |
| 182 | + // Optional cleanup code for globals |
| 183 | +} |
| 184 | + |
| 185 | + |
| 186 | +@_cdecl("get_module") |
| 187 | +@MainActor |
| 188 | +func get_module() -> UnsafeMutablePointer<zend_module_entry> { |
| 189 | + // Allocate memory for myext_functions |
| 190 | + var builder = FunctionListBuilder() |
| 191 | + |
| 192 | + // Add myext_hello |
| 193 | + builder.add( |
| 194 | + name: "myext_hello", |
| 195 | + handler: zif_myext_hello, |
| 196 | + arg_info: arginfo_myext_hello |
| 197 | + ) |
| 198 | + |
| 199 | + // Convert to `UnsafeMutablePointer<zend_function_entry>` |
| 200 | + myext_functions_ptr = builder.build() |
| 201 | + |
| 202 | + // Create PHP Build String |
| 203 | + let version = strdup("1.0.0") |
| 204 | + let module_name = strdup("myext") |
| 205 | + var buildIdString = "API\(PHP_API_VERSION)" |
| 206 | + if ZTS != 0 { |
| 207 | + buildIdString += ",TS" // Thread Safe |
| 208 | + } else { |
| 209 | + buildIdString += ",NTS" // Non-Thread Safe |
| 210 | + } |
| 211 | + if PHP_DEBUG != 0 { |
| 212 | + buildIdString += ",debug" |
| 213 | + } |
| 214 | + let build_id = strdup(buildIdString) |
| 215 | + |
| 216 | + // Setup Custom INI settings |
| 217 | + myext_ini_entries_ptr = UnsafeMutablePointer<zend_ini_entry>.allocate(capacity: 1) |
| 218 | + myext_ini_entries_ptr?.initialize(to: zend_ini_entry()) |
| 219 | + |
| 220 | + // Dependancies |
| 221 | + myext_deps_ptr = UnsafeMutablePointer<zend_module_dep>.allocate(capacity: 1) |
| 222 | + myext_deps_ptr?.initialize(to: zend_module_dep()) |
| 223 | + |
| 224 | + myextModule_ptr = create_module_entry( |
| 225 | + module_name, |
| 226 | + version, |
| 227 | + myext_functions_ptr, |
| 228 | + zm_startup_myext, |
| 229 | + zm_shutdown_myext, |
| 230 | + zm_activate_myext, |
| 231 | + zm_deactivate_myext, |
| 232 | + zm_info_myext, |
| 233 | + MemoryLayout<myextGlobals>.size, |
| 234 | + &myext_globals_id, |
| 235 | + zm_globals_ctor_myext, |
| 236 | + zm_globals_dtor_myext, |
| 237 | + build_id |
| 238 | + ) |
| 239 | + |
| 240 | + return myextModule_ptr! |
| 241 | +} |
| 242 | + |
| 243 | + |
| 244 | + |
| 245 | +``` |
0 commit comments