diff --git a/examples/objective-c-macos/.gitignore b/examples/objective-c-macos/.gitignore
index da2cb165..014c319e 100644
--- a/examples/objective-c-macos/.gitignore
+++ b/examples/objective-c-macos/.gitignore
@@ -1,5 +1,7 @@
/build/
/build-*/
+/ios-build/
/dist/
/*.rsp
/typephp_macos_hello
+/typephp_ios_hello
diff --git a/examples/objective-c-macos/Info-iOS.plist b/examples/objective-c-macos/Info-iOS.plist
new file mode 100644
index 00000000..61df13f3
--- /dev/null
+++ b/examples/objective-c-macos/Info-iOS.plist
@@ -0,0 +1,42 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ en
+ CFBundleDisplayName
+ TypePHP Hello
+ CFBundleExecutable
+ typephp_ios_hello
+ CFBundleIdentifier
+ org.swoole.typephp.ios-hello
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ TypePHP iOS Hello
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ 0.1.0
+ CFBundleVersion
+ 1
+ LSRequiresIPhoneOS
+
+ MinimumOSVersion
+ 15.0
+ UIDeviceFamily
+
+ 1
+
+ UILaunchScreen
+
+ UIRequiredDeviceCapabilities
+
+ arm64
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+
+
+
diff --git a/examples/objective-c-macos/README.md b/examples/objective-c-macos/README.md
index ab1692a9..7ff7515c 100644
--- a/examples/objective-c-macos/README.md
+++ b/examples/objective-c-macos/README.md
@@ -1,10 +1,12 @@
-# TypePHP macOS AppKit example
+# TypePHP Apple native GUI examples
-This is a native macOS GUI application built from TypePHP and Objective-C++.
-TypePHP defines the window layout, application state, click behavior and event
-loop. Objective-C++ remains a thin platform bridge that exposes primitive AppKit
-operations. Clicking the native button returns a control ID to TypePHP, which
-updates the status label.
+This directory contains matching macOS AppKit and iPhoneOS UIKit applications.
+Both targets reuse `php-src/application.php`: TypePHP defines the window layout,
+application state and click behavior, while Objective-C++ only exposes small
+native UI operations. Clicking the native button returns a control ID to
+TypePHP, which updates the status label.
+
+## macOS
Requirements:
@@ -33,8 +35,65 @@ sh examples/objective-c-macos/package-app.sh
open "examples/objective-c-macos/dist/TypePHP macOS Hello.app"
```
-The project uses an `.mm` source because PHPX exposes a C++ API. Its project
-configuration enables ARC and links both AppKit and Foundation. This example is
-macOS-only; an iOS application would additionally require Xcode project and
-code-signing integration, so it is intentionally not mixed into this desktop
-example.
+## iPhoneOS (physical iPhone)
+
+The iPhone build is cross-compiled on macOS. It requires full Xcode (Command
+Line Tools alone do not contain the iPhoneOS SDK), an Apple Development signing
+identity, a provisioning profile for `org.swoole.typephp.ios-hello`, and an
+`iphoneos-arm64` TypePHP SDK with this layout:
+
+```text
+/
+├── include/php/...
+├── lib/
+│ ├── libphp.a
+│ ├── libphpx.a
+│ ├── libgmp.a
+│ ├── libgmpxx.a
+│ └── libmpfr.a
+└── .typephp-ios-sdk-abi
+```
+
+The PHP and every third-party archive in this prefix must be built for
+`arm64-apple-ios`; a macOS/Homebrew archive cannot be linked into an iPhoneOS
+binary. Apple system libraries and UIKit/Foundation remain SDK frameworks and
+are linked by Xcode rather than copied into `libphp.a`.
+
+After selecting full Xcode, verify the SDK:
+
+```sh
+sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
+xcrun --sdk iphoneos --show-sdk-path
+```
+
+Install this SDK at `ios/iphoneos-arm64` inside the PHPX checkout, matching the
+integrated layouts already used by `full-static/sdk` and
+`wasm/wasm32-wasip2`. Build the executable from the TypePHP repository root;
+`PHPX_HOME` selects both PHPX sources and the target SDK:
+
+```sh
+export PHPX_HOME=/path/to/phpx
+php bin/tpc.php examples/objective-c-macos/ios.yml --no-progress
+```
+
+The compiler produces `examples/objective-c-macos/typephp_ios_hello`. Package
+and sign it with a development provisioning profile and identity installed in
+the login keychain:
+
+```sh
+export TYPEPHP_IOS_PROVISIONING_PROFILE=/path/to/profile.mobileprovision
+export TYPEPHP_IOS_CODE_SIGN_IDENTITY='Apple Development: Your Name (TEAMID)'
+sh examples/objective-c-macos/package-ios-app.sh
+```
+
+Find the connected iPhone and install the bundle:
+
+```sh
+xcrun devicectl list devices
+xcrun devicectl device install app \
+ --device \
+ 'examples/objective-c-macos/dist/TypePHP iOS Hello.app'
+```
+
+The `.mm` bridges are deliberately thin. AppKit/UIKit own native controls and
+deliver events, but the shared TypePHP class owns the application behavior.
diff --git a/examples/objective-c-macos/ios-src/uikit_bridge.mm b/examples/objective-c-macos/ios-src/uikit_bridge.mm
new file mode 100644
index 00000000..7a9dea96
--- /dev/null
+++ b/examples/objective-c-macos/ios-src/uikit_bridge.mm
@@ -0,0 +1,169 @@
+#include
+#include
+
+void php_typephp_application_did_launch();
+void php_typephp_application_control_activated(php::Int controlId);
+
+static NSString *to_ns_string(const php::Str &value)
+{
+ NSString *result = [[NSString alloc] initWithBytes:value.data()
+ length:value.length()
+ encoding:NSUTF8StringEncoding];
+ return result == nil ? @"" : result;
+}
+
+@interface TypePHPUIKitBridge : NSObject
+
+@property(nonatomic, strong) UIWindow *window;
+@property(nonatomic, strong) UIViewController *rootViewController;
+@property(nonatomic, strong) NSMutableDictionary *controls;
+@property(nonatomic) NSInteger nextControlId;
+@property(nonatomic) CGFloat logicalWidth;
+@property(nonatomic) CGFloat logicalHeight;
+
+- (void)controlActivated:(UIControl *)sender;
+
+@end
+
+static __strong TypePHPUIKitBridge *bridge = nil;
+static __strong NSString *applicationName = nil;
+
+@implementation TypePHPUIKitBridge
+
+- (instancetype)init
+{
+ self = [super init];
+ if (self != nil) {
+ self.controls = [[NSMutableDictionary alloc] init];
+ self.nextControlId = 1;
+ }
+ return self;
+}
+
+- (BOOL)application:(UIApplication *)application
+ didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
+{
+ (void) application;
+ (void) launchOptions;
+ bridge = self;
+ php_typephp_application_did_launch();
+ return YES;
+}
+
+- (void)controlActivated:(UIControl *)sender
+{
+ php_typephp_application_control_activated(static_cast(sender.tag));
+}
+
+@end
+
+
+static CGRect logical_rect(php::Int x, php::Int y, php::Int width, php::Int height)
+{
+ const CGRect bounds = bridge.rootViewController.view.bounds;
+ const CGFloat scaleX = CGRectGetWidth(bounds) / bridge.logicalWidth;
+ const CGFloat scaleY = CGRectGetHeight(bounds) / bridge.logicalHeight;
+ const CGFloat top = bridge.logicalHeight - static_cast(y + height);
+ return CGRectMake(
+ static_cast(x) * scaleX,
+ top * scaleY,
+ static_cast(width) * scaleX,
+ static_cast(height) * scaleY);
+}
+
+static php::Int store_control(UIView *control)
+{
+ const NSInteger controlId = bridge.nextControlId++;
+ control.tag = controlId;
+ bridge.controls[@(controlId)] = control;
+ return static_cast(controlId);
+}
+
+void php_ui_app_run(php::Str name)
+{
+ @autoreleasepool {
+ applicationName = to_ns_string(name);
+ UIApplicationMain(0, nullptr, nil, NSStringFromClass(TypePHPUIKitBridge.class));
+ }
+}
+
+void php_ui_create_window(php::Str title, php::Int width, php::Int height)
+{
+ @autoreleasepool {
+ (void) title;
+ bridge.logicalWidth = static_cast(width);
+ bridge.logicalHeight = static_cast(height);
+ bridge.window = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds];
+ bridge.rootViewController = [[UIViewController alloc] init];
+ bridge.rootViewController.title = applicationName;
+ bridge.rootViewController.view.backgroundColor = UIColor.systemBackgroundColor;
+ bridge.window.rootViewController = bridge.rootViewController;
+ }
+}
+
+php::Int php_ui_add_label(
+ php::Str text,
+ php::Int x,
+ php::Int y,
+ php::Int width,
+ php::Int height,
+ php::Int fontSize,
+ php::Bool bold)
+{
+ @autoreleasepool {
+ UILabel *label = [[UILabel alloc] initWithFrame:logical_rect(x, y, width, height)];
+ label.text = to_ns_string(text);
+ label.font = bold
+ ? [UIFont systemFontOfSize:static_cast(fontSize)
+ weight:UIFontWeightSemibold]
+ : [UIFont systemFontOfSize:static_cast(fontSize)];
+ label.textAlignment = NSTextAlignmentCenter;
+ label.numberOfLines = 0;
+ label.adjustsFontSizeToFitWidth = YES;
+ label.minimumScaleFactor = 0.75;
+ [bridge.rootViewController.view addSubview:label];
+ return store_control(label);
+ }
+}
+
+php::Int php_ui_add_button(
+ php::Str title,
+ php::Int x,
+ php::Int y,
+ php::Int width,
+ php::Int height)
+{
+ @autoreleasepool {
+ UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
+ button.frame = logical_rect(x, y, width, height);
+ [button setTitle:to_ns_string(title) forState:UIControlStateNormal];
+ button.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold];
+ button.backgroundColor = UIColor.secondarySystemBackgroundColor;
+ button.layer.cornerRadius = 10;
+ [button addTarget:bridge
+ action:@selector(controlActivated:)
+ forControlEvents:UIControlEventTouchUpInside];
+ [bridge.rootViewController.view addSubview:button];
+ return store_control(button);
+ }
+}
+
+void php_ui_set_control_text(php::Int controlId, php::Str text)
+{
+ @autoreleasepool {
+ UIView *control = bridge.controls[@(static_cast(controlId))];
+ NSString *value = to_ns_string(text);
+ if ([control isKindOfClass:UILabel.class]) {
+ ((UILabel *) control).text = value;
+ } else if ([control isKindOfClass:UIButton.class]) {
+ [(UIButton *) control setTitle:value forState:UIControlStateNormal];
+ }
+ }
+}
+
+void php_ui_show_window()
+{
+ @autoreleasepool {
+ [bridge.window makeKeyAndVisible];
+ }
+}
diff --git a/examples/objective-c-macos/ios-src/uikit_bridge.stub.php b/examples/objective-c-macos/ios-src/uikit_bridge.stub.php
new file mode 100644
index 00000000..7ec56f2a
--- /dev/null
+++ b/examples/objective-c-macos/ios-src/uikit_bridge.stub.php
@@ -0,0 +1,23 @@
+(width), static_cast(height));
@@ -103,7 +103,7 @@ void php_macos_create_window(php::Str title, php::Int width, php::Int height)
}
}
-php::Int php_macos_add_label(
+php::Int php_ui_add_label(
php::Str text,
php::Int x,
php::Int y,
@@ -130,7 +130,7 @@ php::Int php_macos_add_label(
}
}
-php::Int php_macos_add_button(
+php::Int php_ui_add_button(
php::Str title,
php::Int x,
php::Int y,
@@ -154,7 +154,7 @@ php::Int php_macos_add_button(
}
}
-void php_macos_set_control_text(php::Int controlId, php::Str text)
+void php_ui_set_control_text(php::Int controlId, php::Str text)
{
@autoreleasepool {
NSControl *control = bridge.controls[@(static_cast(controlId))];
@@ -164,7 +164,7 @@ void php_macos_set_control_text(php::Int controlId, php::Str text)
}
}
-void php_macos_show_window()
+void php_ui_show_window()
{
@autoreleasepool {
[bridge.window makeKeyAndOrderFront:nil];
@@ -173,7 +173,7 @@ void php_macos_show_window()
}
}
-php::Int php_macos_next_event()
+php::Int php_ui_next_event()
{
while (bridge.running) {
@autoreleasepool {
diff --git a/examples/objective-c-macos/objc-src/macos_bridge.stub.php b/examples/objective-c-macos/objc-src/macos_bridge.stub.php
index 10224237..4afc0e9c 100644
--- a/examples/objective-c-macos/objc-src/macos_bridge.stub.php
+++ b/examples/objective-c-macos/objc-src/macos_bridge.stub.php
@@ -1,11 +1,11 @@
&2
+ exit 1
+fi
+
+if [ -z "$profile" ] || [ ! -f "$profile" ]; then
+ echo "Set TYPEPHP_IOS_PROVISIONING_PROFILE to an iOS Development .mobileprovision file." >&2
+ exit 1
+fi
+
+if [ -z "$identity" ]; then
+ echo "Set TYPEPHP_IOS_CODE_SIGN_IDENTITY to an installed Apple Development identity." >&2
+ exit 1
+fi
+
+profile_plist=$(mktemp -t typephp-ios-profile.XXXXXX)
+entitlements=$(mktemp -t typephp-ios-entitlements.XXXXXX)
+trap 'rm -f "$profile_plist" "$entitlements"' EXIT HUP INT TERM
+
+security cms -D -i "$profile" -o "$profile_plist"
+plutil -extract Entitlements xml1 -o "$entitlements" "$profile_plist"
+
+rm -rf "$bundle"
+mkdir -p "$bundle"
+cp "$project_dir/Info-iOS.plist" "$bundle/Info.plist"
+cp "$executable" "$bundle/typephp_ios_hello"
+cp "$profile" "$bundle/embedded.mobileprovision"
+
+codesign \
+ --force \
+ --sign "$identity" \
+ --entitlements "$entitlements" \
+ --timestamp=none \
+ "$bundle"
+codesign --verify --deep --strict --verbose=2 "$bundle"
+
+echo "Created and signed $bundle"
+echo "Install it with: xcrun devicectl device install app --device '$bundle'"
diff --git a/examples/objective-c-macos/php-src/application.php b/examples/objective-c-macos/php-src/application.php
new file mode 100644
index 00000000..c6afffde
--- /dev/null
+++ b/examples/objective-c-macos/php-src/application.php
@@ -0,0 +1,41 @@
+temporaryDirectory('phpx-ios-host');
+ mkdir($phpxDir . '/lib', 0777, true);
+ // A host-side archive with the same name must never win.
+ touch($phpxDir . '/lib/libphpx.a');
+
+ $iosSdk = $phpxDir . '/ios/iphoneos-arm64';
+ mkdir($iosSdk . '/lib', 0777, true);
+ file_put_contents($iosSdk . '/.typephp-ios-sdk-abi', "typephp-iphoneos-arm64-sdk-abi-v1\n");
+ $targetArchive = $iosSdk . '/lib/libphpx.a';
+ touch($targetArchive);
+
+ $restorePhpx = $this->withEnvironment('PHPX_HOME', $phpxDir);
+ $restorePhp = $this->withEnvironment('PHP_HOME', $this->temporaryDirectory('unrelated-host-php'));
+ try {
+ $compiler = $this->newCompiler(new Ios());
+ self::assertSame($targetArchive, $compiler->findPhpxLibraryForTest());
+ self::assertSame($iosSdk, $compiler->getPhpDir());
+ self::assertSame([$iosSdk . '/lib'], $compiler->getLibraryPathsForTest());
+ self::assertSame($iosSdk . '/include/phpx', $compiler->getIncludePathsForTest()[0]);
+ self::assertNotContains($phpxDir . '/include', $compiler->getIncludePathsForTest());
+ self::assertSame(
+ [$targetArchive, 'php', 'gmp', 'gmpxx', 'mpfr', 'c++'],
+ $compiler->getLibrariesForTest(),
+ );
+ self::assertContains('PHPX_IOS=1', $compiler->getCommonCompileOptionsForTest()['user_defines']);
+ } finally {
+ $restorePhp();
+ $restorePhpx();
+ }
+ }
+
public function testNativeModulesDoNotFallBackToStaticPhpx(): void
{
$phpxDir = $this->temporaryDirectory('phpx-static-module');
@@ -175,6 +209,21 @@ final class NativeBuildConfigurationTest extends TestCase
{
return $this->getLibraries();
}
+
+ public function getIncludePathsForTest(): array
+ {
+ return $this->getIncludePaths();
+ }
+
+ public function getLibraryPathsForTest(): array
+ {
+ return $this->getLibraryPaths();
+ }
+
+ public function getCommonCompileOptionsForTest(): array
+ {
+ return $this->getCommonCompileCommandOptions()->toArray();
+ }
};
return $compiler->withPlatform($platform);
@@ -182,13 +231,18 @@ final class NativeBuildConfigurationTest extends TestCase
private function withPhpxHome(string $dir): callable
{
- $previous = getenv('PHPX_HOME');
- putenv('PHPX_HOME=' . $dir);
- return static function () use ($previous): void {
+ return $this->withEnvironment('PHPX_HOME', $dir);
+ }
+
+ private function withEnvironment(string $name, string $value): callable
+ {
+ $previous = getenv($name);
+ putenv($name . '=' . $value);
+ return static function () use ($name, $previous): void {
if ($previous === false) {
- putenv('PHPX_HOME');
+ putenv($name);
} else {
- putenv('PHPX_HOME=' . $previous);
+ putenv($name . '=' . $previous);
}
};
}
diff --git a/phpunit/src/Platform/PlatformTest.php b/phpunit/src/Platform/PlatformTest.php
index 3106e85d..baaad8ae 100644
--- a/phpunit/src/Platform/PlatformTest.php
+++ b/phpunit/src/Platform/PlatformTest.php
@@ -5,6 +5,7 @@ namespace TypePhp\Tests\Platform;
use PHPUnit\Framework\TestCase;
use TypePhp\Platform\Windows;
use TypePhp\Platform\Linux;
+use TypePhp\Platform\Ios;
use TypePhp\Platform\Macos;
use TypePhp\Platform\Wasi;
@@ -22,6 +23,71 @@ class PlatformTest extends TestCase
$this->assertSame([], $platform->getBuildLibraryWarnings('', '', 'bin'));
}
+ public function testIosTargetProperties(): void
+ {
+ $platform = new Ios();
+
+ $this->assertSame('iOS', $platform->getName());
+ $this->assertSame('', $platform->getExecutableExtension());
+ $this->assertSame('.a', $platform->getSharedLibraryExtension());
+ $this->assertSame('xcrun --sdk iphoneos clang++', $platform->getDefaultCompiler());
+ $this->assertSame([], $platform->getDefaultRpaths('/tmp/phpx', '/tmp/php'));
+ $this->assertTrue(Ios::supportsTarget('arm64-apple-ios15.0'));
+ $this->assertTrue(Ios::supportsTarget('aarch64-apple-ios'));
+ $this->assertFalse(Ios::supportsTarget('arm64-apple-darwin'));
+ $this->assertFalse(Ios::supportsTarget('x86_64-apple-ios15.0-simulator'));
+ }
+
+ public function testIosLibraryDiagnosticsUseTargetSdkPrefix(): void
+ {
+ $sdk = sys_get_temp_dir() . '/typephp-ios-sdk-' . bin2hex(random_bytes(6));
+ mkdir($sdk . '/include/php/main', 0777, true);
+ mkdir($sdk . '/include/php/TSRM', 0777, true);
+ mkdir($sdk . '/include/php/Zend', 0777, true);
+ mkdir($sdk . '/include/php/ext/date/lib', 0777, true);
+ mkdir($sdk . '/lib', 0777, true);
+ touch($sdk . '/lib/libphp.a');
+ touch($sdk . '/lib/libphpx.a');
+
+ try {
+ $platform = new Ios();
+ self::assertSame([], $platform->getBuildLibraryWarnings($sdk, '/unused/phpx-home', 'bin'));
+ self::assertSame([$sdk . '/lib'], $platform->buildPhpLibPaths($sdk));
+ self::assertSame(
+ [
+ $sdk . '/include/php',
+ $sdk . '/include/php/main',
+ $sdk . '/include/php/TSRM',
+ $sdk . '/include/php/Zend',
+ $sdk . '/include/php/ext',
+ $sdk . '/include/php/ext/date/lib',
+ ],
+ $platform->buildPhpIncludePaths($sdk),
+ );
+ self::assertSame(
+ [
+ 'embed' => null,
+ 'static' => $sdk . '/lib/libphp.a',
+ 'is_shared' => false,
+ ],
+ $platform->detectPhpLibs($sdk),
+ );
+ } finally {
+ unlink($sdk . '/lib/libphpx.a');
+ unlink($sdk . '/lib/libphp.a');
+ rmdir($sdk . '/lib');
+ rmdir($sdk . '/include/php/ext/date/lib');
+ rmdir($sdk . '/include/php/ext/date');
+ rmdir($sdk . '/include/php/ext');
+ rmdir($sdk . '/include/php/Zend');
+ rmdir($sdk . '/include/php/TSRM');
+ rmdir($sdk . '/include/php/main');
+ rmdir($sdk . '/include/php');
+ rmdir($sdk . '/include');
+ rmdir($sdk);
+ }
+ }
+
/**
* 测试 Windows 平台基本功能
*/
@@ -141,6 +207,7 @@ class PlatformTest extends TestCase
$windows = new Windows();
$linux = new Linux();
$macos = new Macos();
+ $ios = new Ios();
$this->assertSame('.exe', $windows->getTargetExtension('bin'));
$this->assertSame('.dll', $windows->getTargetExtension('ext'));
@@ -151,6 +218,7 @@ class PlatformTest extends TestCase
$this->assertSame('', $macos->getTargetExtension('bin'));
$this->assertSame('.so', $macos->getTargetExtension('ext'));
$this->assertSame('.dylib', $macos->getTargetExtension('lib'));
+ $this->assertSame('', $ios->getTargetExtension('bin'));
}
public function testPlatformPathPrefixRemoval(): void
diff --git a/src/Build/NativeBuildConfigurationTrait.php b/src/Build/NativeBuildConfigurationTrait.php
index a479c9ad..b05e10ec 100644
--- a/src/Build/NativeBuildConfigurationTrait.php
+++ b/src/Build/NativeBuildConfigurationTrait.php
@@ -32,6 +32,42 @@ trait NativeBuildConfigurationTrait
return $sdkDir;
}
+ /**
+ * Resolve the SDK prefix used by targets that cannot consume host PHPX or
+ * host PHP libraries. Keep the target archive and all ABI-sensitive
+ * headers under the same PHPX checkout.
+ */
+ protected function getTargetSdkDir(): ?string
+ {
+ $fullStaticSdk = $this->getFullStaticSdkDir();
+ if ($fullStaticSdk !== null) {
+ return $fullStaticSdk;
+ }
+
+ return $this->isIosTarget() ? $this->getIosSdkDir() : null;
+ }
+
+ protected function getIosSdkDir(): string
+ {
+ $sdkDir = $this->getPhpxDir() . '/ios/iphoneos-arm64';
+ if (!is_dir($sdkDir)) {
+ $this->error(
+ 'The iPhoneOS SDK was not found at: ' . $sdkDir . "\n"
+ . ' Build/install the matching SDK inside PHPX before compiling this target.'
+ );
+ }
+ $abiStamp = $sdkDir . '/.typephp-ios-sdk-abi';
+ if (!is_file($abiStamp)
+ || trim((string) file_get_contents($abiStamp)) !== 'typephp-iphoneos-arm64-sdk-abi-v1'
+ ) {
+ $this->error(
+ 'The iPhoneOS SDK is missing or ABI-incompatible: ' . $sdkDir . "\n"
+ . ' Rebuild it with PHPX ios/build.sh and the matching PHP SDK.'
+ );
+ }
+ return $sdkDir;
+ }
+
/**
* The target triple used for fully-static links.
*
@@ -75,7 +111,7 @@ trait NativeBuildConfigurationTrait
protected function getIncludePaths(): array
{
- $sdkDir = $this->getFullStaticSdkDir();
+ $sdkDir = $this->getTargetSdkDir();
if ($sdkDir !== null) {
return [
$sdkDir . '/include/phpx',
@@ -116,7 +152,7 @@ trait NativeBuildConfigurationTrait
protected function getLibraryPaths(): array
{
- $sdkDir = $this->getFullStaticSdkDir();
+ $sdkDir = $this->getTargetSdkDir();
if ($sdkDir !== null) {
return [$sdkDir . '/lib'];
}
@@ -210,7 +246,7 @@ trait NativeBuildConfigurationTrait
// do. WASI needs nothing — its toolchain supplies the runtime.
if ($this->isLinux()) {
$libraries[] = 'stdc++';
- } elseif ($this->isMacos()) {
+ } elseif ($this->isMacos() || $this->isIosTarget()) {
$libraries[] = 'c++';
}
}
@@ -241,6 +277,15 @@ trait NativeBuildConfigurationTrait
return is_file($phpxLibPath) ? $phpxLibPath : null;
}
+ // An iPhoneOS archive belongs to the cross-compiled PHP SDK, not to
+ // PHPX_HOME/lib where host libraries are installed. Keeping libphp.a
+ // and libphpx.a in the same prefix also prevents a host macOS archive
+ // from being selected accidentally during an iOS link.
+ if ($this->isIosTarget()) {
+ $phpxStaticPath = $this->getPhpDir() . '/lib/libphpx.a';
+ return is_file($phpxStaticPath) ? $phpxStaticPath : null;
+ }
+
// Linux/macOS: prefer the shared library, fall back to the static library
// getSharedLibraryExtension() may or may not include a leading dot, so normalize it
$sharedLibExt = ltrim($platform->getSharedLibraryExtension(), '.');
@@ -270,6 +315,9 @@ trait NativeBuildConfigurationTrait
if ($platform instanceof Windows) {
$expected = $this->getPhpxDir() . '\\lib\\phpx.lib';
$buildHint = 'Build PHPX first (for example, run `nmake phpx` in ' . $this->getPhpxDir() . '\\build)';
+ } elseif ($this->isIosTarget()) {
+ $expected = $this->getPhpDir() . '/lib/libphpx.a';
+ $buildHint = 'Build the integrated iPhoneOS SDK in PHPX_HOME/ios/iphoneos-arm64';
} else {
$sharedLibExt = ltrim($platform->getSharedLibraryExtension(), '.');
$expected = $this->getPhpxDir() . '/lib/libphpx.' . $sharedLibExt;
diff --git a/src/Build/NativeCommandOptionsTrait.php b/src/Build/NativeCommandOptionsTrait.php
index 39499129..0803901e 100644
--- a/src/Build/NativeCommandOptionsTrait.php
+++ b/src/Build/NativeCommandOptionsTrait.php
@@ -23,6 +23,9 @@ trait NativeCommandOptionsTrait
}
$userDefines = $this->userDefines;
+ if ($this->isIosTarget()) {
+ $userDefines[] = 'PHPX_IOS=1';
+ }
if ($this->isBuildModeLib()) {
$userDefines[] = 'TYPEPHP_NO_MAIN=1';
$userDefines[] = $this->getLibraryExportsMacroName() . '=1';
diff --git a/src/CompilerBase.php b/src/CompilerBase.php
index 554875d7..3ef2a3a8 100644
--- a/src/CompilerBase.php
+++ b/src/CompilerBase.php
@@ -67,6 +67,7 @@ use TypePhp\Parser\UnaryExpressionTrait;
use TypePhp\Parser\UniversalMethodCall;
use TypePhp\Optimizer\FuncCallOptimizer;
use TypePhp\Platform\Linux;
+use TypePhp\Platform\Ios;
use TypePhp\Platform\Macos;
use TypePhp\Platform\PlatformBase;
use TypePhp\Platform\PlatformFactory;
@@ -679,6 +680,11 @@ class CompilerBase implements PropertyAccessContext
return $this->getPlatform() instanceof Macos;
}
+ public function isIosTarget(): bool
+ {
+ return $this->getPlatform() instanceof Ios;
+ }
+
public function isWasiTarget(): bool
{
$target = strtolower($this->targetPlatform);
@@ -724,6 +730,12 @@ class CompilerBase implements PropertyAccessContext
public function getPhpDir(): string
{
+ if ($this->isIosTarget()) {
+ // iPhoneOS is a cross target. Its PHP headers and archive are part
+ // of the integrated PHPX SDK, never the host PHP installation.
+ return $this->getIosSdkDir();
+ }
+
try {
return $this->getPlatform()->getPhpDir();
} catch (\RuntimeException $e) {
diff --git a/src/Platform/Ios.php b/src/Platform/Ios.php
new file mode 100644
index 00000000..e2ec7d83
--- /dev/null
+++ b/src/Platform/Ios.php
@@ -0,0 +1,111 @@
+ null,
+ 'static' => $archive,
+ 'is_shared' => false,
+ ];
+ }
+
+ public function getBuildLibraryWarnings(
+ string $phpDir,
+ string $phpxDir,
+ string $buildMode,
+ bool $checkPhpxRuntime = true,
+ ): array {
+ if ($buildMode !== 'bin') {
+ return [];
+ }
+
+ $warnings = [];
+ try {
+ $this->detectPhpLibs($phpDir);
+ } catch (\RuntimeException $exception) {
+ $warnings[] = [
+ 'warning' => 'The iPhoneOS `libphp.a` is not found',
+ 'info' => $exception->getMessage() . '. Rebuild the iPhoneOS SDK in the PHPX directory',
+ ];
+ }
+
+ if (!is_file(rtrim($phpDir, '/') . '/lib/libphpx.a')) {
+ $warnings[] = [
+ 'warning' => 'The iPhoneOS `libphpx.a` is not found',
+ 'info' => 'Build PHPX for iPhoneOS and install it into PHPX_HOME/ios/iphoneos-arm64',
+ ];
+ }
+
+ return $warnings;
+ }
+}
diff --git a/src/Translator.php b/src/Translator.php
index 16547960..1feb84fb 100644
--- a/src/Translator.php
+++ b/src/Translator.php
@@ -42,6 +42,7 @@ use TypePhp\Generator\LibraryImportStubGenerator;
use TypePhp\Generator\Symbol;
use TypePhp\Metadata\Constants;
use TypePhp\Platform\PlatformFactory;
+use TypePhp\Platform\Ios;
use TypePhp\Platform\Wasi;
use TypePhp\Platform\Windows;
use TypePhp\Resolver\Reflection;
@@ -213,7 +214,12 @@ class Translator extends Preprocessor
if ($targetPlatform === 'wasm32-wasip1' || $targetPlatform === 'wasm32-wasi') {
throw new \RuntimeException('WASI Preview 1 is not supported; use wasm32-wasip2');
}
- if ($targetPlatform === 'wasm32-wasip2' || $targetPlatform === 'wasm32-unknown-wasip2') {
+ if (Ios::supportsTarget($targetPlatform)) {
+ if (strtoupper(substr(PHP_OS, 0, 6)) !== 'DARWIN') {
+ throw new \RuntimeException('The iOS target requires a macOS build host with Xcode');
+ }
+ $this->platform = new Ios();
+ } elseif ($targetPlatform === 'wasm32-wasip2' || $targetPlatform === 'wasm32-unknown-wasip2') {
$detectedTarget = getenv('TYPEPHP_WASI_TARGET');
$this->platform = new Wasi(
is_string($detectedTarget) && $detectedTarget !== '' ? $detectedTarget : $targetPlatform,
@@ -1026,7 +1032,7 @@ class Translator extends Preprocessor
$code .= 'extern "C" void save_ps_args(int, char **) {}' . PHP_EOL;
}
- if ($this->isBuildModeBin() && !$this->isWasiTarget()) {
+ if ($this->isBuildModeBin() && !$this->isWasiTarget() && !$this->isIosTarget()) {
$cliHeaders = [
'#include "php_cli_process_title.h"',
'#include "php_cli_process_title_arginfo.h"',
@@ -1225,7 +1231,7 @@ CODE;
$code .= "// clang-format off\n";
$code .= "static const zend_function_entry ext_functions[] = {\n";
- if ($this->isBuildModeBin() && !$this->isWasiTarget()) {
+ if ($this->isBuildModeBin() && !$this->isWasiTarget() && !$this->isIosTarget()) {
$code .= $this->getIndent() . "PHP_FE(cli_set_process_title, arginfo_cli_set_process_title)\n";
$code .= $this->getIndent() . "PHP_FE(cli_get_process_title, arginfo_cli_get_process_title)\n";
}
@@ -1808,7 +1814,7 @@ CODE;
$sourceFiles[] = $this->getPhpxDir() . '/src/misc/typephp_main.cc';
}
- if ($this->isBuildModeBin() && !$this->isWasiTarget()) {
+ if ($this->isBuildModeBin() && !$this->isWasiTarget() && !$this->isIosTarget()) {
$sourceFiles[] = $this->getPhpxDir() . '/src/misc/php_cli_process_title.c';
$sourceFiles[] = $this->getPhpxDir() . '/src/misc/ps_title.c';
}
@@ -2859,6 +2865,15 @@ CODE;
$targetPlatform = $cfg['target-platform'] ?? null;
if (!empty($targetPlatform)) {
$this->targetPlatform = (string) $targetPlatform;
+ if (Ios::supportsTarget($this->targetPlatform)) {
+ if (strtoupper(substr(PHP_OS, 0, 6)) !== 'DARWIN') {
+ $this->error('The iOS target requires a macOS build host with Xcode');
+ }
+ $this->platform = new Ios();
+ $this->compilerBackend = null;
+ $this->cppCompiler = $this->platform->getDefaultCompiler();
+ $this->initializeNewArchitecture();
+ }
}
// Read build-dir