feat(macos): add Objective-C++ macOS AppKit example application

- Create native macOS GUI application using TypePHP and Objective-C++ bridge
- Implement thin wrappers around AppKit API for window management and controls
- Add support for labels, buttons, and event handling in native macOS interface
- Include proper Info.plist configuration and code signing script for app bundles
- Provide complete example with application lifecycle and user interaction
- Document build requirements and packaging instructions for macOS distribution
master
韩天峰 1 month ago
parent 6215ecd06d
commit 622d6b0a00
  1. 5
      examples/objective-c-macos/.gitignore
  2. 26
      examples/objective-c-macos/Info.plist
  3. 40
      examples/objective-c-macos/README.md
  4. 197
      examples/objective-c-macos/objc-src/macos_bridge.mm
  5. 25
      examples/objective-c-macos/objc-src/macos_bridge.stub.php
  6. 19
      examples/objective-c-macos/package-app.sh
  7. 38
      examples/objective-c-macos/php-src/main.php
  8. 16
      examples/objective-c-macos/project.yml

@ -0,0 +1,5 @@
/build/
/build-*/
/dist/
/*.rsp
/typephp_macos_hello

@ -0,0 +1,26 @@
<?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>CFBundleExecutable</key>
<string>typephp-macos-hello</string>
<key>CFBundleIdentifier</key>
<string>org.swoole.typephp.macos-hello</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>TypePHP macOS Hello</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSMinimumSystemVersion</key>
<string>11.0</string>
<key>NSHighResolutionCapable</key>
<true/>
</dict>
</plist>

@ -0,0 +1,40 @@
# TypePHP macOS AppKit example
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.
Requirements:
- macOS with Xcode Command Line Tools
- PHP 8.4 or 8.5 built with the embed SAPI (`libphp.dylib`)
- a matching PHPX build
- `PHPX_HOME` pointing to the PHPX source tree
Build from the TypePHP repository root:
```sh
export PHPX_HOME=/path/to/phpx
php bin/tpc.php examples/objective-c-macos/project.yml --no-progress
```
Run the native executable:
```sh
./examples/objective-c-macos/typephp_macos_hello
```
To create a standard `.app` bundle that can be launched from Finder:
```sh
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.

@ -0,0 +1,197 @@
#include <AppKit/AppKit.h>
#include <phpx.h>
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 TypePHPAppKitBridge : NSObject <NSWindowDelegate>
@property(nonatomic, strong) NSWindow *window;
@property(nonatomic, strong) NSMutableDictionary<NSNumber *, NSControl *> *controls;
@property(nonatomic) NSInteger nextControlId;
@property(nonatomic) NSInteger pendingControlId;
@property(nonatomic) BOOL running;
- (void)controlActivated:(NSControl *)sender;
@end
@implementation TypePHPAppKitBridge
- (instancetype)init
{
self = [super init];
if (self != nil) {
self.controls = [[NSMutableDictionary alloc] init];
self.nextControlId = 1;
}
return self;
}
- (void)controlActivated:(NSControl *)sender
{
self.pendingControlId = sender.tag;
}
- (void)windowWillClose:(NSNotification *)notification
{
(void) notification;
self.running = NO;
}
@end
static __strong TypePHPAppKitBridge *bridge = nil;
static void install_application_menu(NSString *applicationName)
{
NSMenu *menuBar = [[NSMenu alloc] init];
NSMenuItem *applicationMenuItem = [[NSMenuItem alloc] init];
[menuBar addItem:applicationMenuItem];
NSApp.mainMenu = menuBar;
NSMenu *applicationMenu = [[NSMenu alloc] init];
NSString *quitTitle = [@"Quit " stringByAppendingString:applicationName];
NSMenuItem *quitItem = [[NSMenuItem alloc] initWithTitle:quitTitle
action:@selector(terminate:)
keyEquivalent:@"q"];
[applicationMenu addItem:quitItem];
applicationMenuItem.submenu = applicationMenu;
}
static NSInteger store_control(NSControl *control)
{
const NSInteger controlId = bridge.nextControlId++;
control.tag = controlId;
bridge.controls[@(controlId)] = control;
return controlId;
}
void php_macos_app_init(php::Str applicationName)
{
@autoreleasepool {
NSApplication *application = NSApplication.sharedApplication;
application.activationPolicy = NSApplicationActivationPolicyRegular;
bridge = [[TypePHPAppKitBridge alloc] init];
install_application_menu(to_ns_string(applicationName));
[application finishLaunching];
}
}
void php_macos_create_window(php::Str title, php::Int width, php::Int height)
{
@autoreleasepool {
const NSRect frame = NSMakeRect(0, 0, static_cast<CGFloat>(width), static_cast<CGFloat>(height));
const NSWindowStyleMask style = NSWindowStyleMaskTitled |
NSWindowStyleMaskClosable |
NSWindowStyleMaskMiniaturizable |
NSWindowStyleMaskResizable;
bridge.window = [[NSWindow alloc] initWithContentRect:frame
styleMask:style
backing:NSBackingStoreBuffered
defer:NO];
bridge.window.releasedWhenClosed = NO;
bridge.window.delegate = bridge;
bridge.window.title = to_ns_string(title);
bridge.window.minSize = NSMakeSize(480, 300);
[bridge.window center];
}
}
php::Int php_macos_add_label(
php::Str text,
php::Int x,
php::Int y,
php::Int width,
php::Int height,
php::Int fontSize,
php::Bool bold)
{
@autoreleasepool {
NSTextField *label = [NSTextField wrappingLabelWithString:to_ns_string(text)];
label.frame = NSMakeRect(
static_cast<CGFloat>(x),
static_cast<CGFloat>(y),
static_cast<CGFloat>(width),
static_cast<CGFloat>(height));
label.font = bold
? [NSFont systemFontOfSize:static_cast<CGFloat>(fontSize)
weight:NSFontWeightSemibold]
: [NSFont systemFontOfSize:static_cast<CGFloat>(fontSize)];
label.alignment = NSTextAlignmentCenter;
label.autoresizingMask = NSViewWidthSizable | NSViewMinYMargin;
[bridge.window.contentView addSubview:label];
return static_cast<php::Int>(store_control(label));
}
}
php::Int php_macos_add_button(
php::Str title,
php::Int x,
php::Int y,
php::Int width,
php::Int height)
{
@autoreleasepool {
NSButton *button = [NSButton buttonWithTitle:to_ns_string(title)
target:bridge
action:@selector(controlActivated:)];
button.frame = NSMakeRect(
static_cast<CGFloat>(x),
static_cast<CGFloat>(y),
static_cast<CGFloat>(width),
static_cast<CGFloat>(height));
button.bezelStyle = NSBezelStyleRounded;
button.keyEquivalent = @"\r";
button.autoresizingMask = NSViewMinXMargin | NSViewMaxXMargin | NSViewMinYMargin;
[bridge.window.contentView addSubview:button];
return static_cast<php::Int>(store_control(button));
}
}
void php_macos_set_control_text(php::Int controlId, php::Str text)
{
@autoreleasepool {
NSControl *control = bridge.controls[@(static_cast<NSInteger>(controlId))];
if (control != nil) {
control.stringValue = to_ns_string(text);
}
}
}
void php_macos_show_window()
{
@autoreleasepool {
[bridge.window makeKeyAndOrderFront:nil];
bridge.running = YES;
[NSApp activateIgnoringOtherApps:YES];
}
}
php::Int php_macos_next_event()
{
while (bridge.running) {
@autoreleasepool {
NSEvent *event = [NSApp nextEventMatchingMask:NSEventMaskAny
untilDate:NSDate.distantFuture
inMode:NSDefaultRunLoopMode
dequeue:YES];
if (event != nil) {
[NSApp sendEvent:event];
[NSApp updateWindows];
}
if (bridge.pendingControlId != 0) {
const NSInteger controlId = bridge.pendingControlId;
bridge.pendingControlId = 0;
return static_cast<php::Int>(controlId);
}
}
}
return -1;
}

@ -0,0 +1,25 @@
<?php
/** Thin wrappers around the macOS AppKit API. */
function macos_app_init(string $applicationName): void {}
function macos_create_window(string $title, int $width, int $height): void {}
function macos_add_label(
string $text,
int $x,
int $y,
int $width,
int $height,
int $fontSize,
bool $bold
): int {}
function macos_add_button(string $title, int $x, int $y, int $width, int $height): int {}
function macos_set_control_text(int $controlId, string $text): void {}
function macos_show_window(): void {}
/** Return the activated control ID, or -1 after the window is closed. */
function macos_next_event(): int {}

@ -0,0 +1,19 @@
#!/bin/sh
set -eu
project_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
executable="$project_dir/typephp_macos_hello"
bundle="$project_dir/dist/TypePHP macOS Hello.app"
if [ ! -x "$executable" ]; then
echo "Missing $executable; build project.yml first." >&2
exit 1
fi
mkdir -p "$bundle/Contents/MacOS"
cp "$project_dir/Info.plist" "$bundle/Contents/Info.plist"
cp "$executable" "$bundle/Contents/MacOS/typephp-macos-hello"
codesign --force --deep --sign - "$bundle"
echo "Created $bundle"

@ -0,0 +1,38 @@
<?php
function main(): void
{
macos_app_init('TypePHP macOS Hello');
macos_create_window('TypePHP macOS Hello', 640, 380);
macos_add_label('TypePHP + Objective-C++', 44, 292, 552, 40, 28, true);
macos_add_label(
"The application state and event loop are implemented in TypePHP.\n" .
'Objective-C++ only provides thin wrappers around AppKit.',
64,
178,
512,
88,
15,
false,
);
$statusLabel = macos_add_label('Waiting for an AppKit event.', 64, 130, 512, 24, 13, false);
$button = macos_add_button('Send event to TypePHP', 210, 64, 220, 40);
$clickCount = 0;
macos_show_window();
while (true) {
$controlId = macos_next_event();
if ($controlId < 0) {
break;
}
if ($controlId !== $button) {
continue;
}
$clickCount++;
$suffix = $clickCount === 1 ? 'time' : 'times';
macos_set_control_text($statusLabel, "Button clicked $clickCount $suffix; handled by TypePHP.");
}
}

@ -0,0 +1,16 @@
name: objective_c_macos
version: 0.1.0
mode: bin
output: typephp_macos_hello
cxx-std: c++17
sources:
- php-src
- objc-src
cxx-flags:
- -fobjc-arc
ld-flags:
- -framework AppKit
- -framework Foundation
Loading…
Cancel
Save