Files
ppsspp/ios/main.mm
T
Henrik RydgårdandClaude Opus 5.5 81efd601f0 iOS: Merge the Metal view controller into the base one
With the GL view controller gone, PPSSPPViewControllerMetal was the only
subclass of PPSSPPBaseViewController. They're now one class,
PPSSPPViewController, in ios/ViewController.mm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 16:13:29 +02:00

698 lines
21 KiB
Plaintext

// main.mm boilerplate
//
// Overview
//
// main.mm: JIT enablement, the System_ functions, starting the next step
// AppDelegate.mm: Audio session interruptions, deep link and library export helpers
// SceneDelegate.mm: Runs NativeInit, creates the window and the view controller
// ViewController.mm: The view controller. Rendering (Vulkan to a CAMetalLayer), input, keyboard, pickers and sizing
#import <UIKit/UIKit.h>
#import <dlfcn.h>
#import <mach/mach.h>
#import <mach-o/loader.h>
#import <mach-o/getsect.h>
#import <pthread.h>
#import <spawn.h>
#import <signal.h>
#import <string>
#import <stdio.h>
#import <stdlib.h>
#import <sys/syscall.h>
#import "AppDelegate.h"
#import "ViewController.h"
#import "iOSCoreAudio.h"
#import "IAPManager.h"
#import "SceneDelegate.h"
#include "Common/MemoryUtil.h"
#include "Common/Audio/AudioBackend.h"
#include "Common/System/NativeApp.h"
#include "Common/System/System.h"
#include "Common/System/Request.h"
#include "Common/StringUtils.h"
#include "Common/Profiler/Profiler.h"
#include "Common/Thread/ThreadUtil.h"
#include "Common/System/Display.h"
#include "Core/Config.h"
#include "Common/Log.h"
#include "Common/Log/LogManager.h"
#include "Core/Util/DarwinFileSystemServices.h"
// Compile out all the hackery in app store builds.
#if !PPSSPP_PLATFORM(IOS_APP_STORE)
struct cs_blob_index {
uint32_t type;
uint32_t offset;
};
struct cs_superblob {
uint32_t magic;
uint32_t length;
uint32_t count;
struct cs_blob_index index[];
};
struct cs_entitlements {
uint32_t magic;
uint32_t length;
char entitlements[];
};
static int (*csops)(pid_t pid, unsigned int ops, void * useraddr, size_t usersize);
static boolean_t (*exc_server)(mach_msg_header_t *, mach_msg_header_t *);
static int (*ptrace)(int request, pid_t pid, caddr_t addr, int data);
#define CS_OPS_STATUS 0 /* return status */
#define CS_KILL 0x00000200 /* kill process if it becomes invalid */
#define CS_DEBUGGED 0x10000000 /* process is currently or has previously been debugged and allowed to run with invalid pages */
#define PT_TRACE_ME 0 /* child declares it's being traced */
#define PT_SIGEXC 12 /* signals as exceptions for current_proc */
#define ptrace(a, b, c, d) syscall(SYS_ptrace, a, b, c, d)
static void *exception_handler(void *argument) {
mach_port_t port = *(mach_port_t *)argument;
mach_msg_server(exc_server, 2048, port, 0);
return NULL;
}
static NSDictionary *parse_entitlements(const void *entitlements, size_t length) {
char *copy = (char *)malloc(length);
memcpy(copy, entitlements, length);
NSData *data = [NSData dataWithBytes:copy length:length];
free(copy);
return [NSPropertyListSerialization propertyListWithData:data
options:NSPropertyListImmutable
format:nil
error:nil];
}
static NSDictionary *app_entitlements(void) {
// Inspired by codesign.c in Darwin sources for Security.framework
// Find our mach-o header
Dl_info dl_info;
if (dladdr((const void *)app_entitlements, &dl_info) == 0)
return nil;
if (dl_info.dli_fbase == NULL)
return nil;
char *base = (char *)dl_info.dli_fbase;
struct mach_header_64 *header = (struct mach_header_64 *)dl_info.dli_fbase;
if (header->magic != MH_MAGIC_64)
return nil;
// Simulator executables have fake entitlements in the code signature. The real entitlements can be found in an __entitlements section.
size_t entitlements_size;
uint8_t *entitlements_data = getsectiondata(header, "__TEXT", "__entitlements", &entitlements_size);
if (entitlements_data != NULL) {
NSData *data = [NSData dataWithBytesNoCopy:entitlements_data
length:entitlements_size
freeWhenDone:NO];
return [NSPropertyListSerialization propertyListWithData:data
options:NSPropertyListImmutable
format:nil
error:nil];
}
// Find the LC_CODE_SIGNATURE
struct load_command *lc = (struct load_command *) (base + sizeof(*header));
struct linkedit_data_command *cs_lc = NULL;
for (uint32_t i = 0; i < header->ncmds; i++) {
if (lc->cmd == LC_CODE_SIGNATURE) {
cs_lc = (struct linkedit_data_command *) lc;
break;
}
lc = (struct load_command *) ((char *) lc + lc->cmdsize);
}
if (cs_lc == NULL)
return nil;
// Read the code signature off disk, as it's apparently not loaded into memory
NSFileHandle *fileHandle = [NSFileHandle fileHandleForReadingFromURL:NSBundle.mainBundle.executableURL error:nil];
if (fileHandle == nil)
return nil;
[fileHandle seekToFileOffset:cs_lc->dataoff];
NSData *csData = [fileHandle readDataOfLength:cs_lc->datasize];
[fileHandle closeFile];
const struct cs_superblob *cs = (const struct cs_superblob *)csData.bytes;
if (ntohl(cs->magic) != 0xfade0cc0)
return nil;
// Find the entitlements in the code signature
for (uint32_t i = 0; i < ntohl(cs->count); i++) {
struct cs_entitlements *ents = (struct cs_entitlements *) ((char *) cs + ntohl(cs->index[i].offset));
if (ntohl(ents->magic) == 0xfade7171) {
return parse_entitlements(ents->entitlements, ntohl(ents->length) - offsetof(struct cs_entitlements, entitlements));
}
}
return nil;
}
static NSDictionary *cached_app_entitlements(void) {
static NSDictionary *entitlements = nil;
if (!entitlements) {
entitlements = app_entitlements();
}
return entitlements;
}
bool jb_has_container(void) {
NSDictionary *entitlements = cached_app_entitlements();
return ![entitlements[@"com.apple.private.security.no-sandbox"] boolValue];
}
static char *childArgv[] = {NULL, "debugme", NULL};
bool jb_spawn_ptrace_child(int argc, char **argv) {
int ret; pid_t pid;
if (argc > 1 && strcmp(argv[1], childArgv[1]) == 0) {
ret = ptrace(PT_TRACE_ME, 0, NULL, 0);
NSLog(@"child: ptrace(PT_TRACE_ME) %d", ret);
exit(ret);
}
if (jb_has_container()) {
return false;
}
childArgv[0] = argv[0];
if ((ret = posix_spawnp(&pid, argv[0], NULL, NULL, (char *const *)childArgv, NULL)) != 0) {
return false;
}
return true;
}
bool jb_has_jit_entitlement(void) {
NSDictionary *entitlements = cached_app_entitlements();
return [entitlements[@"dynamic-codesigning"] boolValue];
}
bool jb_has_cs_disabled(void) {
int flags;
return !csops(getpid(), CS_OPS_STATUS, &flags, sizeof(flags)) && (flags & ~CS_KILL) == flags;
}
static bool jb_has_debugger_attached(void) {
int flags;
return !csops(getpid(), CS_OPS_STATUS, &flags, sizeof(flags)) && flags & CS_DEBUGGED;
}
bool jb_enable_ptrace_hack(void) {
bool debugged = jb_has_debugger_attached();
// Thanks to this comment: https://news.ycombinator.com/item?id=18431524
// We use this hack to allow mmap with PROT_EXEC (which usually requires the
// dynamic-codesigning entitlement) by tricking the process into thinking
// that Xcode is debugging it. We abuse the fact that JIT is needed to
// debug the process.
if (ptrace(PT_TRACE_ME, 0, NULL, 0) < 0) {
return false;
}
// ptracing ourselves confuses the kernel and will cause bad things to
// happen to the system (hangs…) if an exception or signal occurs. Setup
// some "safety nets" so we can cause the process to exit in a somewhat sane
// state. We only need to do this if the debugger isn't attached. (It'll do
// this itself, and if we do it we'll interfere with its normal operation
// anyways.)
if (!debugged) {
// First, ensure that signals are delivered as Mach software exceptions…
ptrace(PT_SIGEXC, 0, NULL, 0);
// …then ensure that this exception goes through our exception handler.
// I think it's OK to just watch for EXC_SOFTWARE because the other
// exceptions (e.g. EXC_BAD_ACCESS, EXC_BAD_INSTRUCTION, and friends)
// will end up being delivered as signals anyways, and we can get them
// once they're resent as a software exception.
mach_port_t port = MACH_PORT_NULL;
mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &port);
mach_port_insert_right(mach_task_self(), port, port, MACH_MSG_TYPE_MAKE_SEND);
task_set_exception_ports(mach_task_self(), EXC_MASK_SOFTWARE, port, EXCEPTION_DEFAULT, THREAD_STATE_NONE);
pthread_t thread;
pthread_create(&thread, NULL, exception_handler, (void *)&port);
}
return true;
}
#endif
float g_safeInsetLeft = 0.0;
float g_safeInsetRight = 0.0;
float g_safeInsetTop = 0.0;
float g_safeInsetBottom = 0.0;
static bool g_jitAvailable = true;
static int g_iosVersionMajor;
static std::string version;
std::string System_GetProperty(SystemProperty prop) {
switch (prop) {
case SYSPROP_NAME:
return StringFromFormat("iOS %s", version.c_str());
case SYSPROP_LANGREGION:
return [[[NSLocale currentLocale] objectForKey:NSLocaleIdentifier] UTF8String];
case SYSPROP_BUILD_VERSION:
return PPSSPP_GIT_VERSION;
default:
return "";
}
}
std::vector<std::string> System_GetPropertyStringVec(SystemProperty prop) {
switch (prop) {
case SYSPROP_TEMP_DIRS:
default:
return std::vector<std::string>();
}
}
extern "C" {
int Apple_GetCurrentBatteryCapacity();
}
int64_t System_GetPropertyInt(SystemProperty prop) {
switch (prop) {
case SYSPROP_AUDIO_SAMPLE_RATE:
return 44100;
case SYSPROP_DEVICE_TYPE:
return DEVICE_TYPE_MOBILE;
case SYSPROP_SYSTEMVERSION:
return g_iosVersionMajor;
case SYSPROP_BATTERY_PERCENTAGE:
return Apple_GetCurrentBatteryCapacity();
default:
return -1;
}
}
float System_GetPropertyFloat(SystemProperty prop) {
switch (prop) {
case SYSPROP_DISPLAY_REFRESH_RATE:
return 60.f;
case SYSPROP_DISPLAY_SAFE_INSET_LEFT:
return g_safeInsetLeft * g_display.dpi_scale_x;
case SYSPROP_DISPLAY_SAFE_INSET_RIGHT:
return g_safeInsetRight * g_display.dpi_scale_x;
case SYSPROP_DISPLAY_SAFE_INSET_TOP:
return g_safeInsetTop * g_display.dpi_scale_y;
case SYSPROP_DISPLAY_SAFE_INSET_BOTTOM:
return g_safeInsetBottom * g_display.dpi_scale_y;
default:
return -1;
}
}
bool System_GetPropertyBool(SystemProperty prop) {
switch (prop) {
case SYSPROP_HAS_DEEP_LINKS:
return true;
case SYSPROP_CAN_LAUNCH_URL:
return true;
case SYSPROP_DISPLAY_HAS_CAMERA_CUTOUT:
return true;
case SYSPROP_HAS_FILE_BROWSER:
return true;
case SYSPROP_HAS_FOLDER_BROWSER:
return true;
case SYSPROP_HAS_IMAGE_BROWSER:
return true;
case SYSPROP_HAS_OPEN_DIRECTORY:
return false;
case SYSPROP_HAS_BACK_BUTTON:
return false;
case SYSPROP_HAS_ACCELEROMETER:
return true;
case SYSPROP_HAS_KEYBOARD:
return true;
case SYSPROP_SUPPORTS_SHARE_TEXT:
case SYSPROP_SUPPORTS_SHARE_FILE:
return true;
case SYSPROP_KEYBOARD_IS_SOFT:
// If a hardware keyboard is connected, and we add support, we could return false here.
return true;
case SYSPROP_APP_GOLD:
#ifdef GOLD
// This is deprecated.
return true;
#elif PPSSPP_PLATFORM(IOS_APP_STORE)
// Check the IAP status.
return [[IAPManager sharedIAPManager] isGoldUnlocked];
#else
return false;
#endif
case SYSPROP_USE_IAP:
#if PPSSPP_PLATFORM(IOS_APP_STORE) && defined(USE_IAP)
return true;
#else
return false;
#endif
case SYSPROP_CAN_JIT:
return g_jitAvailable;
case SYSPROP_LIMITED_FILE_BROWSING:
#if PPSSPP_PLATFORM(IOS_APP_STORE)
return true;
#else
return false; // But will return true in app store builds.
#endif
#ifndef HTTPS_NOT_AVAILABLE
case SYSPROP_SUPPORTS_HTTPS:
return true;
#endif
case SYSPROP_CAN_READ_BATTERY_PERCENTAGE:
return true;
case SYSPROP_CAN_RESTRICT_ORIENTATION:
return true;
case SYSPROP_CAN_GET_FREE_SPACE_FAST:
return false;
case SYSPROP_MICROPHONE_NEEDS_POLLING:
return true;
default:
return false;
}
}
void System_Notify(SystemNotification notification) {
switch (notification) {
case SystemNotification::APP_SWITCH_MODE_CHANGED:
dispatch_async(dispatch_get_main_queue(), ^{
if (sharedViewController) {
[sharedViewController appSwitchModeChanged];
}
});
break;
case SystemNotification::IMMERSIVE_MODE_CHANGE:
dispatch_async(dispatch_get_main_queue(), ^{
if (sharedViewController) {
[sharedViewController immersiveModeChanged];
}
});
break;
case SystemNotification::UI_STATE_CHANGED:
dispatch_async(dispatch_get_main_queue(), ^{
if (sharedViewController) {
[sharedViewController uiStateChanged];
}
});
break;
case SystemNotification::AUDIO_MODE_CHANGED:
dispatch_async(dispatch_get_main_queue(), ^{
iOSCoreAudioUpdateSession();
});
break;
case SystemNotification::ROTATE_UPDATED:
dispatch_async(dispatch_get_main_queue(), ^{
if (sharedViewController) {
INFO_LOG(Log::System, "Requesting device orientation update");
if (@available(iOS 16.0, *)) {
[sharedViewController setNeedsUpdateOfSupportedInterfaceOrientations];
} else {
[UIViewController attemptRotationToDeviceOrientation];
}
}
});
break;
default:
break;
}
}
bool System_MakeRequest(SystemRequestType type, int requestId, const std::string &param1, const std::string &param2, int64_t param3, int64_t param4) {
switch (type) {
case SystemRequestType::RESTART_APP:
{
// A copy, since the block outlives param1.
std::string args = param1;
dispatch_async(dispatch_get_main_queue(), ^{
// Get the connected scenes
NSSet<UIScene *> *scenes = [UIApplication sharedApplication].connectedScenes;
// Loop through scenes to find a UIWindowScene that is active
for (UIScene *scene in scenes) {
if ([scene isKindOfClass:[UIWindowScene class]]) {
UIWindowScene *windowScene = (UIWindowScene *)scene;
SceneDelegate *sceneDelegate = (SceneDelegate *)windowScene.delegate;
[sceneDelegate restart:args.c_str()];
break; // call only on the first active scene
}
}
});
return true;
}
case SystemRequestType::EXIT_APP:
// NOTE: on iOS, this is considered a crash and not a valid way to exit.
exit(0);
// The below seems right, but causes hangs. See #12140.
// dispatch_async(dispatch_get_main_queue(), ^{
// [sharedViewController shutdown];
// exit(0);
// });
break;
case SystemRequestType::BROWSE_FOR_FILE:
{
DarwinDirectoryPanelCallback callback = [requestId] (bool success, Path path) {
if (success) {
g_requestManager.PostSystemSuccess(requestId, path.c_str());
} else {
g_requestManager.PostSystemFailure(requestId);
}
};
DarwinFileSystemServices::presentDirectoryPanel(callback, /* allowFiles = */ true, /* allowDirectories = */ false);
return true;
}
case SystemRequestType::BROWSE_FOR_FOLDER:
{
DarwinDirectoryPanelCallback callback = [requestId] (bool success, Path path) {
if (success) {
g_requestManager.PostSystemSuccess(requestId, path.c_str());
} else {
g_requestManager.PostSystemFailure(requestId);
}
};
DarwinFileSystemServices::presentDirectoryPanel(callback, /* allowFiles = */ false, /* allowDirectories = */ true);
return true;
}
case SystemRequestType::BROWSE_FOR_IMAGE:
{
NSString *filename = [NSString stringWithUTF8String:param2.c_str()];
dispatch_async(dispatch_get_main_queue(), ^{
[sharedViewController pickPhoto:filename requestId:requestId];
});
return true;
}
case SystemRequestType::CAMERA_COMMAND:
{
// These come from the emu thread. UIKit and Core Location want the main thread, and that's
// also the only thread that changes sharedViewController.
std::string command = param1;
dispatch_async(dispatch_get_main_queue(), ^{
if (!strncmp(command.c_str(), "startVideo", 10)) {
int width = 0, height = 0;
sscanf(command.c_str(), "startVideo_%dx%d", &width, &height);
[sharedViewController startVideo:width height:height];
} else if (command == "stopVideo") {
[sharedViewController stopVideo];
}
});
return true;
}
case SystemRequestType::GPS_COMMAND:
{
std::string command = param1;
dispatch_async(dispatch_get_main_queue(), ^{
if (command == "open") {
[sharedViewController startLocation];
} else if (command == "close") {
[sharedViewController stopLocation];
}
});
return true;
}
case SystemRequestType::MICROPHONE_COMMAND:
// From the CPU thread. Polling has to happen right here, the rest moves to the main thread inside.
if (startsWith(param1, "startRecording:")) {
int sampleRate = 0;
if (sscanf(param1.c_str(), "startRecording:%d", &sampleRate) != 1 || sampleRate <= 0) {
return false;
}
iOSCoreAudioStartRecording(sampleRate);
} else if (param1 == "stopRecording") {
iOSCoreAudioStopRecording();
} else if (param1 == "pollRecording") {
iOSCoreAudioPollRecording();
} else {
return false;
}
return true;
case SystemRequestType::SHARE_TEXT:
{
NSString *text = [NSString stringWithUTF8String:param1.c_str()];
dispatch_async(dispatch_get_main_queue(), ^{
[sharedViewController shareText:text];
});
return true;
}
case SystemRequestType::SHARE_FILE:
{
// param2, the MIME type, isn't needed. The share sheet goes by the extension.
NSString *path = [NSString stringWithUTF8String:param1.c_str()];
if (![[NSFileManager defaultManager] fileExistsAtPath:path]) {
ERROR_LOG(Log::System, "SHARE_FILE: '%s' doesn't exist", param1.c_str());
return false;
}
NSURL *url = [NSURL fileURLWithPath:path];
dispatch_async(dispatch_get_main_queue(), ^{
[sharedViewController shareFile:url];
});
return true;
}
case SystemRequestType::NOTIFY_UI_EVENT:
{
const UIEventNotification event = (UIEventNotification)param3;
dispatch_async(dispatch_get_main_queue(), ^{
switch (event) {
case UIEventNotification::POPUP_CLOSED:
case UIEventNotification::DIALOG_CLOSED:
case UIEventNotification::TEXT_LOSTFOCUS:
[sharedViewController hideKeyboard];
break;
case UIEventNotification::TEXT_GOTFOCUS:
[sharedViewController showKeyboard];
break;
default:
break;
}
});
return true;
}
#if PPSSPP_PLATFORM(IOS_APP_STORE)
case SystemRequestType::IAP_RESTORE_PURCHASES:
{
[[IAPManager sharedIAPManager] restorePurchasesWithRequestID:requestId];
return true;
}
case SystemRequestType::IAP_MAKE_PURCHASE:
{
[[IAPManager sharedIAPManager] buyGoldWithRequestID:requestId];
return true;
}
#endif
case SystemRequestType::COPY_TO_CLIPBOARD:
{
NSString *text = [NSString stringWithUTF8String:param1.c_str()];
dispatch_async(dispatch_get_main_queue(), ^{
[UIPasteboard generalPasteboard].string = text;
});
return true;
}
case SystemRequestType::SET_KEEP_SCREEN_BRIGHT:
dispatch_async(dispatch_get_main_queue(), ^{
INFO_LOG(Log::System, "SET_KEEP_SCREEN_BRIGHT: %d", (int)param3);
[[UIApplication sharedApplication] setIdleTimerDisabled: (param3 ? YES : NO)];
});
return true;
default:
break;
}
return false;
}
void System_Toast(std::string_view text) {}
void System_AskForPermission(SystemPermission permission) {}
void System_LaunchUrl(LaunchUrlType urlType, std::string_view url) {
std::string strUrl(url);
NSURL *nsUrl = [NSURL URLWithString:[NSString stringWithUTF8String:strUrl.c_str()]];
if (!nsUrl) {
ERROR_LOG(Log::System, "Failed to parse URL: %s", strUrl.c_str());
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
[[UIApplication sharedApplication] openURL:nsUrl options:@{} completionHandler:nil];
});
}
bool System_AudioRecordingIsAvailable() {
// Every iPhone and iPad has a microphone. Permission is asked for when recording starts.
return true;
}
PermissionStatus System_GetPermissionStatus(SystemPermission permission) {
return PERMISSION_STATUS_GRANTED;
}
void System_Vibrate(int mode) {
// Comes from the UI code on the emu thread, and UIKit wants the main thread.
dispatch_async(dispatch_get_main_queue(), ^{
// Does nothing on devices without a Taptic Engine.
static UISelectionFeedbackGenerator *feedbackGenerator;
if (!feedbackGenerator) {
feedbackGenerator = [[UISelectionFeedbackGenerator alloc] init];
[feedbackGenerator prepare];
}
[feedbackGenerator selectionChanged];
});
}
AudioBackend *System_CreateAudioBackend() {
// Use legacy mechanisms.
return nullptr;
}
int main(int argc, char *argv[]) {
version = [[[UIDevice currentDevice] systemVersion] UTF8String];
if (1 != sscanf(version.c_str(), "%d", &g_iosVersionMajor)) {
// Just set it to 15 (the minimum) if the parsing fails for whatever reason.
g_iosVersionMajor = 15;
}
g_logManager.EnableOutput(LogOutput::Stdio);
#if PPSSPP_PLATFORM(IOS_APP_STORE)
g_jitAvailable = false;
#else
// Hacky hacks to try to enable JIT by pretending to be a debugger.
csops = reinterpret_cast<decltype(csops)>(dlsym(dlopen(nullptr, RTLD_LAZY), "csops"));
exc_server = reinterpret_cast<decltype(exc_server)>(dlsym(dlopen(NULL, RTLD_LAZY), "exc_server"));
ptrace = reinterpret_cast<decltype(ptrace)>(dlsym(dlopen(NULL, RTLD_LAZY), "ptrace"));
// see https://github.com/hrydgard/ppsspp/issues/11905
if (jb_spawn_ptrace_child(argc, argv)) {
INFO_LOG(Log::System, "JIT: ptrace() child spawn trick\n");
} else if (jb_has_jit_entitlement()) {
INFO_LOG(Log::System, "JIT: found entitlement\n");
} else if (jb_has_cs_disabled()) {
INFO_LOG(Log::System, "JIT: CS_KILL disabled\n");
} else if (jb_enable_ptrace_hack()) {
INFO_LOG(Log::System, "JIT: ptrace() hack supported\n");
} else {
INFO_LOG(Log::System, "JIT: ptrace() hack failed\n");
g_jitAvailable = false;
}
// Tried checking for JIT support here with AllocateExecutableMemory and ProtectMemoryPages,
// but it just succeeds, and then fails when you try to execute from it.
#endif
// Ignore sigpipe.
if (signal(SIGPIPE, SIG_IGN) == SIG_ERR) {
perror("Unable to ignore SIGPIPE");
}
PROFILE_INIT();
#if PPSSPP_PLATFORM(IOS_APP_STORE) && defined(_DEBUG)
g_logManager.SetOutputsEnabled(LogOutput::Stdio);
#endif
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}