- Add Ios platform class with iPhoneOS cross-compilation support - Implement iOS-specific build configuration and library handling - Add iOS build example with UIKit bridge implementation - Update compiler base to recognize iOS target platform - Add iOS-specific gitignore entries and build directories - Implement unified UI bridge functions with platform-agnostic naming - Add iOS project configuration and packaging script - Include iOS platform tests and documentation updatesmaster
parent
622d6b0a00
commit
b356636853
20 changed files with 782 additions and 66 deletions
@ -1,5 +1,7 @@ |
||||
/build/ |
||||
/build-*/ |
||||
/ios-build/ |
||||
/dist/ |
||||
/*.rsp |
||||
/typephp_macos_hello |
||||
/typephp_ios_hello |
||||
|
||||
@ -0,0 +1,42 @@ |
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
||||
<plist version="1.0"> |
||||
<dict> |
||||
<key>CFBundleDevelopmentRegion</key> |
||||
<string>en</string> |
||||
<key>CFBundleDisplayName</key> |
||||
<string>TypePHP Hello</string> |
||||
<key>CFBundleExecutable</key> |
||||
<string>typephp_ios_hello</string> |
||||
<key>CFBundleIdentifier</key> |
||||
<string>org.swoole.typephp.ios-hello</string> |
||||
<key>CFBundleInfoDictionaryVersion</key> |
||||
<string>6.0</string> |
||||
<key>CFBundleName</key> |
||||
<string>TypePHP iOS Hello</string> |
||||
<key>CFBundlePackageType</key> |
||||
<string>APPL</string> |
||||
<key>CFBundleShortVersionString</key> |
||||
<string>0.1.0</string> |
||||
<key>CFBundleVersion</key> |
||||
<string>1</string> |
||||
<key>LSRequiresIPhoneOS</key> |
||||
<true/> |
||||
<key>MinimumOSVersion</key> |
||||
<string>15.0</string> |
||||
<key>UIDeviceFamily</key> |
||||
<array> |
||||
<integer>1</integer> |
||||
</array> |
||||
<key>UILaunchScreen</key> |
||||
<dict/> |
||||
<key>UIRequiredDeviceCapabilities</key> |
||||
<array> |
||||
<string>arm64</string> |
||||
</array> |
||||
<key>UISupportedInterfaceOrientations</key> |
||||
<array> |
||||
<string>UIInterfaceOrientationPortrait</string> |
||||
</array> |
||||
</dict> |
||||
</plist> |
||||
@ -0,0 +1,169 @@ |
||||
#include <UIKit/UIKit.h> |
||||
#include <phpx.h> |
||||
|
||||
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 <UIApplicationDelegate> |
||||
|
||||
@property(nonatomic, strong) UIWindow *window; |
||||
@property(nonatomic, strong) UIViewController *rootViewController; |
||||
@property(nonatomic, strong) NSMutableDictionary<NSNumber *, UIView *> *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<UIApplicationLaunchOptionsKey, id> *)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<php::Int>(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<CGFloat>(y + height); |
||||
return CGRectMake( |
||||
static_cast<CGFloat>(x) * scaleX, |
||||
top * scaleY, |
||||
static_cast<CGFloat>(width) * scaleX, |
||||
static_cast<CGFloat>(height) * scaleY); |
||||
} |
||||
|
||||
static php::Int store_control(UIView *control) |
||||
{ |
||||
const NSInteger controlId = bridge.nextControlId++; |
||||
control.tag = controlId; |
||||
bridge.controls[@(controlId)] = control; |
||||
return static_cast<php::Int>(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<CGFloat>(width); |
||||
bridge.logicalHeight = static_cast<CGFloat>(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<CGFloat>(fontSize) |
||||
weight:UIFontWeightSemibold] |
||||
: [UIFont systemFontOfSize:static_cast<CGFloat>(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<NSInteger>(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]; |
||||
} |
||||
} |
||||
@ -0,0 +1,23 @@ |
||||
<?php |
||||
|
||||
/** Enter the UIKit-owned iOS application lifecycle. */ |
||||
function ui_app_run(string $applicationName): void {} |
||||
|
||||
/** Thin wrappers around UIKit; coordinates use the shared 640x380 logical canvas. */ |
||||
function ui_create_window(string $title, int $width, int $height): void {} |
||||
|
||||
function ui_add_label( |
||||
string $text, |
||||
int $x, |
||||
int $y, |
||||
int $width, |
||||
int $height, |
||||
int $fontSize, |
||||
bool $bold |
||||
): int {} |
||||
|
||||
function ui_add_button(string $title, int $x, int $y, int $width, int $height): int {} |
||||
|
||||
function ui_set_control_text(int $controlId, string $text): void {} |
||||
|
||||
function ui_show_window(): void {} |
||||
@ -0,0 +1,24 @@ |
||||
name: typephp_ios_hello |
||||
version: 0.1.0 |
||||
mode: bin |
||||
output: typephp_ios_hello |
||||
php-version: "8.5" |
||||
cxx-std: c++17 |
||||
cpp-compiler: xcrun --sdk iphoneos clang++ |
||||
target-platform: arm64-apple-ios15.0 |
||||
build-dir: ios-build |
||||
|
||||
sources: |
||||
- php-src/application.php |
||||
- php-src/ios-main.php |
||||
- ios-src |
||||
|
||||
cxx-flags: |
||||
- -fobjc-arc |
||||
- -miphoneos-version-min=15.0 |
||||
|
||||
ld-flags: |
||||
- -miphoneos-version-min=15.0 |
||||
- -Wl,-dead_strip |
||||
- -framework UIKit |
||||
- -framework Foundation |
||||
@ -0,0 +1,48 @@ |
||||
#!/bin/sh |
||||
|
||||
set -eu |
||||
|
||||
project_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) |
||||
executable="$project_dir/typephp_ios_hello" |
||||
bundle="$project_dir/dist/TypePHP iOS Hello.app" |
||||
profile=${TYPEPHP_IOS_PROVISIONING_PROFILE:-} |
||||
identity=${TYPEPHP_IOS_CODE_SIGN_IDENTITY:-} |
||||
|
||||
if [ ! -x "$executable" ]; then |
||||
echo "Missing $executable; build ios.yml first." >&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 <device-id> '$bundle'" |
||||
@ -0,0 +1,41 @@ |
||||
<?php |
||||
|
||||
final class HelloApplication |
||||
{ |
||||
private static int $statusLabel = 0; |
||||
private static int $button = 0; |
||||
private static int $clickCount = 0; |
||||
|
||||
public static function build(string $platformApi): void |
||||
{ |
||||
ui_create_window('TypePHP Native Hello', 640, 380); |
||||
ui_add_label('TypePHP + Objective-C++', 44, 292, 552, 40, 28, true); |
||||
ui_add_label( |
||||
"The interface and application state are implemented in TypePHP.\n" . |
||||
"Objective-C++ only provides thin wrappers around $platformApi.", |
||||
64, |
||||
178, |
||||
512, |
||||
88, |
||||
15, |
||||
false, |
||||
); |
||||
|
||||
self::$statusLabel = ui_add_label('Waiting for a native UI event.', 64, 130, 512, 24, 13, false); |
||||
self::$button = ui_add_button('Send event to TypePHP', 210, 64, 220, 40); |
||||
} |
||||
|
||||
public static function handleEvent(int $controlId): void |
||||
{ |
||||
if ($controlId !== self::$button) { |
||||
return; |
||||
} |
||||
|
||||
self::$clickCount++; |
||||
$suffix = self::$clickCount === 1 ? 'time' : 'times'; |
||||
ui_set_control_text( |
||||
self::$statusLabel, |
||||
'Button clicked ' . self::$clickCount . " $suffix; handled by TypePHP.", |
||||
); |
||||
} |
||||
} |
||||
@ -0,0 +1,19 @@ |
||||
<?php |
||||
|
||||
/** Called by the thin UIKit application delegate after iOS finishes launching. */ |
||||
function typephp_application_did_launch(): void |
||||
{ |
||||
HelloApplication::build('UIKit'); |
||||
ui_show_window(); |
||||
} |
||||
|
||||
/** Called by the UIKit target-action bridge for every activated control. */ |
||||
function typephp_application_control_activated(int $controlId): void |
||||
{ |
||||
HelloApplication::handleEvent($controlId); |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
ui_app_run('TypePHP iOS Hello'); |
||||
} |
||||
@ -0,0 +1,111 @@ |
||||
<?php |
||||
|
||||
namespace TypePhp\Platform; |
||||
|
||||
/** |
||||
* iPhoneOS cross-compilation target hosted on macOS. |
||||
* |
||||
* iOS executables link PHP and PHPX statically and must not inherit Homebrew |
||||
* include paths, library paths, or runtime rpaths from the macOS host. |
||||
*/ |
||||
class Ios extends UnixPlatform |
||||
{ |
||||
public static function supportsTarget(string $target): bool |
||||
{ |
||||
return preg_match('/^(?:arm64|aarch64)-apple-ios(?:\d+(?:\.\d+)*)?$/i', $target) === 1; |
||||
} |
||||
|
||||
public function getName(): string |
||||
{ |
||||
return 'iOS'; |
||||
} |
||||
|
||||
public function isCurrent(): bool |
||||
{ |
||||
return false; |
||||
} |
||||
|
||||
public function getSharedLibraryExtension(): string |
||||
{ |
||||
return '.a'; |
||||
} |
||||
|
||||
public function getDefaultCompiler(): string |
||||
{ |
||||
return 'xcrun --sdk iphoneos clang++'; |
||||
} |
||||
|
||||
public function getDefaultRpaths(?string $phpxDir = null, ?string $phpDir = null): array |
||||
{ |
||||
return []; |
||||
} |
||||
|
||||
/** |
||||
* An iPhoneOS SDK is a target sysroot, not a runnable PHP installation. |
||||
* Its path is resolved from PHPX_HOME by CompilerBase, and php-config is |
||||
* never executed for the target. |
||||
*/ |
||||
public function buildPhpIncludePaths(string $phpDir): array |
||||
{ |
||||
$paths = [ |
||||
$phpDir . '/include/php', |
||||
$phpDir . '/include/php/main', |
||||
$phpDir . '/include/php/TSRM', |
||||
$phpDir . '/include/php/Zend', |
||||
$phpDir . '/include/php/ext', |
||||
$phpDir . '/include/php/ext/date/lib', |
||||
]; |
||||
|
||||
return array_values(array_filter($paths, is_dir(...))); |
||||
} |
||||
|
||||
public function buildPhpLibPaths(string $phpDir): array |
||||
{ |
||||
$libDir = rtrim($phpDir, '/') . '/lib'; |
||||
return is_dir($libDir) ? [$libDir] : []; |
||||
} |
||||
|
||||
public function detectPhpLibs(string $phpDir): array |
||||
{ |
||||
$archive = rtrim($phpDir, '/') . '/lib/libphp.a'; |
||||
if (!is_file($archive)) { |
||||
throw new \RuntimeException("The iPhoneOS libphp.a was not found: {$archive}"); |
||||
} |
||||
|
||||
return [ |
||||
'embed' => 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; |
||||
} |
||||
} |
||||
Loading…
Reference in new issue