1/* 2 * JavaAppLauncher: a simple Java application launcher for Mac OS X. 3 * JavaParamBlock.mm 4 * 5 * Paul J. Lucas [paul@lightcrafts.com] 6 * 7 * This code is heavily based on: 8 * http://developer.apple.com/samplecode/JavaSplashScreen/JavaSplashScreen.html 9 */ 10 11#import <stdlib.h> /* for getenv(3) */ 12#import <string.h> 13#import <unistd.h> /* for chdir(2) */ 14#import <Cocoa/Cocoa.h> 15#import <mach-o/arch.h> /* for NXGetLocalArchInfo(3) */ 16#import <sys/sysctl.h> /* for sysctl(3) */ 17 18#import "JavaParamBlock.h" 19#import "LC_CocoaUtils.h" 20#import "SetJVMVersion.h" 21#import "UI.h" 22 23using namespace std; 24using namespace LightCrafts; 25 26/** 27 * If there is no previously set user preference for the maximum amount of 28 * memory to use, then this is the percentage of the physical memory that 29 * should be used. 30 */ 31float const DefaultMaxMemoryPercentage = 0.3; 32 33/** 34 * Insist on at least this much RAM. 35 */ 36int const JavaMinMemoryInMB = 512; 37 38/** 39 * Limit the JVM heap size because we now use non-heap memory for things like 40 * tile caches. 41 */ 42#ifdef __LP64__ 43int const JavaMaxMemoryInMB = 32768; 44#else 45int const JavaMaxMemoryInMB = 2048; 46#endif 47 48extern cpu_type_t lc_cpuType; 49 50/** 51 * Copy a C string to a newly allocated C buffer. 52 */ 53static void new_strcpy( char **dest, char const *src ) { 54 if ( src ) { 55 *dest = new char[ ::strlen( src ) + 1 ]; 56 if ( !*dest ) 57 LC_die( @"Unexpected", @"Could not allocate memory" ); 58 ::strcpy( *dest, src ); 59 } else { 60 *dest = new char[1]; 61 if ( !*dest ) 62 LC_die( @"Unexpected", @"Could not allocate memory" ); 63 **dest = '\0'; 64 } 65} 66 67/** 68 * Convert/copy an NSString to a newly allocated C string. 69 */ 70static void new_strcpy( char **dest, NSString const *src ) { 71 if ( src ) 72 new_strcpy( dest, [src UTF8String] ); 73 else 74 new_strcpy( dest, static_cast<char const*>( 0 ) ); 75} 76 77/** 78 * Allocate and copy a JVM option string. 79 */ 80inline void newJVMOption( JavaVMOption *jvm_opt, char const *s ) { 81 new_strcpy( &jvm_opt->optionString, s ); 82 jvm_opt->extraInfo = 0; 83} 84 85/** 86 * Allocate and copy a JVM option string. 87 */ 88static void newJVMOption( JavaVMOption *jvm_opt, NSString const *s ) { 89 if ( s ) 90 newJVMOption( jvm_opt, [s UTF8String] ); 91 else 92 newJVMOption( jvm_opt, static_cast<char const*>( 0 ) ); 93} 94 95/** 96 * Resolve all references to $APP_PACKAGE, $JAVAROOT, and $LC_JVM_VERSION. 97 */ 98static NSString* resolveString( NSString *in, NSDictionary const *javaDict ) { 99 if ( in == nil ) 100 return in; 101 102 // Make a mutable copy of the string to work on. 103 NSMutableString *const temp = [NSMutableString string]; 104 [temp appendString:in]; 105 106 // 107 // First do $APP_PACKAGE. 108 // 109 NSString *const appPackage = [[NSBundle mainBundle] bundlePath]; 110 if ( appPackage ) 111 [temp replaceOccurrencesOfString:@"$APP_PACKAGE" withString:appPackage 112 options:NSLiteralSearch range:NSMakeRange( 0, [temp length] )]; 113 114 // 115 // Next do $JAVAROOT. 116 // 117 NSMutableString *const javaRoot = [NSMutableString string]; 118 NSString *const javaRootProp = [javaDict objectForKey:@"$JAVAROOT"]; 119 if ( javaRootProp ) 120 [javaRoot appendString:javaRootProp]; 121 else 122 [javaRoot appendString:@"Contents/Resources/Java"]; 123 [temp replaceOccurrencesOfString:@"$JAVAROOT" withString:javaRoot 124 options:NSLiteralSearch range:NSMakeRange( 0, [temp length] )]; 125 126 // 127 // Finally, $LC_JVM_VERSION. 128 // 129 NSString *const jvmVersion = 130 [NSString stringWithUTF8String: ::getenv( "JAVA_JVM_VERSION" )]; 131 [temp replaceOccurrencesOfString:@"$LC_JVM_VERSION" withString:jvmVersion 132 options:NSLiteralSearch range:NSMakeRange( 0, [temp length] )]; 133 134 return temp; 135} 136 137/** 138 * Set the given JVM option string. 139 */ 140inline void setJVMOption( JavaVMOption *jvm_opt, char const *s ) { 141 jvm_opt->optionString = const_cast<char*>( s ); 142 jvm_opt->extraInfo = 0; 143} 144 145/** 146 * Set the given JVM option string. 147 */ 148static void setJVMOption( JavaVMOption *jvm_opt, NSString const *s ) { 149 if ( s ) 150 setJVMOption( jvm_opt, [s UTF8String] ); 151 else 152 setJVMOption( jvm_opt, static_cast<char const*>( 0 ) ); 153} 154 155/** 156 * Gets the class path from the "ClassPath" key of the Java dictionary in the 157 * application's Info.plist file. 158 */ 159static NSString* getClassPath( NSDictionary const *javaDict ) { 160 id classPathProp = [javaDict objectForKey:@"ClassPath"]; 161 if ( classPathProp == nil ) 162 return nil; 163 164 // 165 // The class path must be passed to the JVM using the java.class.path 166 // property. The JVM doesn't accept either the -cp or -classpath options. 167 // 168 NSMutableString *const javaClassPath = [NSMutableString string]; 169 [javaClassPath appendString:@"-Djava.class.path="]; 170 171 if ( [classPathProp isKindOfClass:[NSString class]] ) { 172 // 173 // It's just a single string. 174 // 175 [javaClassPath appendString:classPathProp]; 176 } else if ( [classPathProp isKindOfClass:[NSArray class]] ) { 177 // 178 // It's an array of strings. 179 // 180 int const n = [classPathProp count]; 181 for ( int i = 0; i < n; ++i ) { 182 if ( i > 0 ) 183 [javaClassPath appendString:@":"]; 184 [javaClassPath appendString:[classPathProp objectAtIndex:i]]; 185 } 186 } else 187 LC_die( @"Corrupted", @"Bad ClassPath" ); 188 189 return resolveString( javaClassPath, javaDict ); 190} 191 192/** 193 * Gets the working directory from the "WorkingDirectory" key, if any, of the 194 * Java dictionary in the application's Info.plist file. 195 * 196 * Also sets the current working directory to "WorkingDirectory" if specified 197 * or the application bundle's path if not. 198 */ 199static NSString* getCWD( NSDictionary const *javaDict ) { 200 // 201 // First check to see if the key WorkingDirectory is defined in the Java 202 // dictionary. 203 // 204 NSString *cwd = [javaDict objectForKey:@"WorkingDirectory"]; 205 206 if ( cwd ) 207 cwd = resolveString( cwd, javaDict ); 208 else // Default to the path to the application's bundle. 209 cwd = [[NSBundle mainBundle] bundlePath]; 210 211 if ( ::chdir( [cwd fileSystemRepresentation] ) != 0 ) 212 LC_die( @"Unexpected", @"Set CWD failed" ); 213 214 return cwd; 215} 216 217/** 218 * Add the VM options for the given key. 219 */ 220static void addVMOptions( NSMutableArray *options, 221 NSDictionary const *javaDict, 222 NSString const *vmOptionsKey ) { 223 if ( id const vmArgs = [javaDict objectForKey:vmOptionsKey] ) 224 if ( [vmArgs isKindOfClass:[NSString class]] ) 225 [options addObject:vmArgs]; 226 else if ( [vmArgs isKindOfClass:[NSArray class]] ) 227 [options addObjectsFromArray:vmArgs]; 228 else 229 LC_die( @"Corrupted", @"Bad VMOptions" ); 230} 231 232/** 233 * Allocate and initialize an array of JavaVMOption data structures from the 234 * Java dictionary in the application's Info.plist file. 235 */ 236static int getJVMOptions( JavaVMOption **jvm_options, 237 NSDictionary const *javaDict ) { 238 NSMutableArray *const options = [NSMutableArray arrayWithCapacity:1]; 239 240 // 241 // Process the VMOptions. 242 // 243 addVMOptions( options, javaDict, @"VMOptions" ); 244 switch ( lc_cpuType ) { 245 case CPU_TYPE_I386: 246 addVMOptions( options, javaDict, @"LC_VMOptionsX86" ); 247 break; 248 case CPU_TYPE_POWERPC: 249 addVMOptions( options, javaDict, @"LC_VMOptionsPPC" ); 250 break; 251 } 252 253 // 254 // Add the java.class.path property. 255 // 256 NSString *const classPath = getClassPath( javaDict ); 257 if ( classPath ) 258 [options addObject:classPath]; 259 260 // 261 // Set the working directory (pwd). 262 // 263 NSMutableString *const userDir = [NSMutableString string]; 264 [userDir appendString:@"-Duser.dir="]; 265 [userDir appendString:getCWD( javaDict )]; 266 [userDir appendString:@"/"]; 267 [options addObject:userDir]; 268 269 // 270 // Add the properties defined in Properties dictionary. 271 // 272 NSDictionary const *const propDict = [javaDict objectForKey:@"Properties"]; 273 if ( propDict ) { 274 NSArray const *const keys = [propDict allKeys]; 275 int const n = [keys count]; 276 for ( int i = 0; i < n; ++i ) { 277 NSString *const key = [keys objectAtIndex:i]; 278 NSMutableString *const prop = [NSMutableString string]; 279 [prop appendString:@"-D"]; 280 [prop appendString:key]; 281 [prop appendString:@"="]; 282 [prop appendString: 283 resolveString( [propDict objectForKey:key], javaDict )]; 284 [options addObject:prop]; 285 } 286 } 287 288 // 289 // Convert the NSMutableArray into an array of JavaVMOptions. 290 // 291 int const n = [options count]; 292 if ( n > 0 ) { 293 *jvm_options = new JavaVMOption[ n ]; 294 for ( int i = 0; i < n; ++i ) 295 newJVMOption( &(*jvm_options)[i], [options objectAtIndex:i] ); 296 } else 297 *jvm_options = 0; 298 299 return n; 300} 301 302/** 303 * Gets the desired Java version from the "JVMVersion" key of the Java 304 * dictionary in the application's Info.plist file. 305 */ 306static void getJVMVersion( char **dest, NSDictionary const *javaDict ) { 307 NSString const *const jvmVersion = [javaDict objectForKey:@"JVMVersion"]; 308 if ( !jvmVersion ) 309 LC_die( @"Corrupted", @"Missing JVMVersion" ); 310 new_strcpy( dest, jvmVersion ); 311} 312 313/** 314 * Get the arguments to main() from the "Arguments" key of the Java dictionary 315 * in the application's Info.plist file. 316 */ 317static int getMainArgs( char ***main_argv, NSDictionary const *javaDict ) { 318 int n; 319 id const args = [javaDict objectForKey:@"Arguments"]; 320 if ( args ) { 321 if ( [args isKindOfClass:[NSString class]] ) { 322 // 323 // The "Arguments" key has only a single string value. 324 // 325 n = 1; 326 *main_argv = new char*[1]; 327 if ( !*main_argv ) 328 LC_die( @"Unexpected", @"Could not allocate main() array" ); 329 new_strcpy( &(*main_argv)[0], args ); 330 } else if ( [args isKindOfClass:[NSArray class]] ) { 331 // 332 // The "Arguments" key is an array of strings. 333 // 334 n = [args count]; 335 *main_argv = new char*[n]; 336 if ( !*main_argv ) 337 LC_die( @"Unexpected", @"Could not allocate main() array" ); 338 for ( int i = 0; i < n; ++i ) { 339 id const arg = [args objectAtIndex:i]; 340 if ( ![arg isKindOfClass:[NSString class]] ) 341 LC_die( @"Corrupted", @"Bad argument array" ); 342 new_strcpy( &(*main_argv)[i], arg ); 343 } 344 } else 345 LC_die( @"Corrupted", @"Bad Arguments" ); 346 } else { 347 *main_argv = 0; 348 n = 0; 349 } 350 return n; 351} 352 353/** 354 * Get the name of the class whose main() is to be executed from the Java 355 * dictionary in the application's Info.plist file. 356 */ 357static void getMainClassName( char **dest, NSDictionary const *javaDict ) { 358 NSString const *const mainClassName = [javaDict objectForKey:@"MainClass"]; 359 if ( !mainClassName ) 360 LC_die( @"Corrupted", @"Missing MainClass" ); 361 new_strcpy( dest, mainClassName ); 362 // 363 // Convert a class name of the form com.foo.bar to com/foo/bar because the 364 // latter is how FindClass() wants it. Curiously, this step isn't in 365 // Apple's sample code. Hmmm.... 366 // 367 for ( char *c = *dest; *c; ++c ) 368 if ( *c == '.' ) 369 *c = '/'; 370} 371 372/** 373 * Read the maxmemory preference from Java. 374 */ 375static int getMaxMemoryFromPreference() { 376 // 377 // TODO: this should be replaced with the proper Cocoa API for accessing 378 // user preferences since the path ~/Library/Preferences is probably in the 379 // user's native language. 380 // 381 char const *const home = ::getenv( "HOME" ); 382 if ( !home ) 383 return 0; 384 NSMutableString *const prefFile = 385 [NSMutableString stringWithUTF8String:home]; 386 [prefFile appendString:@"/Library/Preferences/com.lightcrafts.app.plist"]; 387 NSDictionary const *const plistDict = 388 [NSDictionary dictionaryWithContentsOfFile:prefFile]; 389 if ( !plistDict ) 390 return 0; 391 NSDictionary const *const appDict = 392 [plistDict objectForKey:@"/com/lightcrafts/app/"]; 393 if ( !appDict ) 394 return 0; 395 NSString const *const maxMemory = [appDict objectForKey:@"MaxMemory"]; 396 if ( !maxMemory ) 397 return 0; 398 return (int)::strtol( [maxMemory UTF8String], NULL, 10 ); 399} 400 401/** 402 * Get the value of the MaxMemory user preference in megabytes; if none, 403 * default to some percentage of physical memory (but do not exceed what the 404 * JVM can handle). 405 */ 406static int getMaxMemory() { 407 int memInMB = getMaxMemoryFromPreference(); 408 if ( !memInMB ) { 409 int sysParam[] = { CTL_HW, HW_MEMSIZE }; 410 // 411 // Be defensive and allow for the possibility that sysctl(3) might 412 // return either a 32- or 64-bit result by using a union and checking 413 // the size of the result and using the correct union member. 414 // 415 // See: 416 // http://www.cocoabuilder.com/archive/message/cocoa/2004/5/6/106388 417 // 418 union { 419 uint32_t ui32; 420 uint64_t ui64; 421 } result; 422 size_t resultSize = sizeof( result ); 423 424 ::sysctl( sysParam, 2, &result, &resultSize, NULL, 0 ); 425 memInMB = (int)( 426 ( resultSize == sizeof( result.ui32 ) ? 427 (result.ui32 / 1048576) : (result.ui64 / 1048576) 428 ) * DefaultMaxMemoryPercentage 429 ); 430 } 431 432 if ( memInMB < JavaMinMemoryInMB ) 433 return JavaMinMemoryInMB; 434 if ( memInMB > JavaMaxMemoryInMB ) 435 return JavaMaxMemoryInMB; 436 return memInMB; 437} 438 439/** 440 * Replace any -Xmx option that may have been specified in the Info.plist file 441 * with one generated from the user preference. 442 */ 443static void replaceXmxOption( JavaVMInitArgs *jvm_args ) { 444 NSString const *const jvmXmxOption = 445 [NSString stringWithFormat:@"-Xmx%dm", getMaxMemory()]; 446 if ( jvm_args->nOptions ) { 447 // 448 // There is at lease one JVM option: loop through them all looking for 449 // an -Xmx option. If found, replace it. 450 // 451 for ( int i = 0; i < jvm_args->nOptions; ++i ) { 452 char **const optionString = &jvm_args->options[i].optionString; 453 if ( ::strncmp( *optionString, "-Xmx", 4 ) == 0 ) { 454 delete[] *optionString; 455 new_strcpy( optionString, jvmXmxOption ); 456 return; 457 } 458 } 459 // 460 // No existing -Xmx option was found: we need to append one. 461 // 462 int const new_nOptions = jvm_args->nOptions + 1; 463 JavaVMOption *const new_options = new JavaVMOption[ new_nOptions ]; 464 int i; 465 for ( i = 0; i < jvm_args->nOptions; ++i ) 466 setJVMOption( &new_options[i], jvm_args->options[i].optionString ); 467 newJVMOption( &new_options[i], jvmXmxOption ); 468 delete[] jvm_args->options; 469 jvm_args->options = new_options; 470 jvm_args->nOptions = new_nOptions; 471 } else { 472 // 473 // There are no JVM options: create one for the -Xmx option. 474 // 475 jvm_args->nOptions = 1; 476 jvm_args->options = new JavaVMOption[1]; 477 newJVMOption( &jvm_args->options[0], jvmXmxOption ); 478 } 479} 480 481/** 482 * Initialize the given JavaParamBlock from the application's Info.plist file. 483 */ 484void initJavaParamBlock( JavaParamBlock *jpb ) { 485 auto_obj<NSAutoreleasePool> pool; 486 487 NSDictionary const *const javaDict = 488 [[[NSBundle mainBundle] infoDictionary] objectForKey:@"Java"]; 489 if ( !javaDict ) 490 LC_die( @"Corrupted", @"Missing Java dictionary" ); 491 492 // 493 // Set-up the desired JVM version. 494 // 495 getJVMVersion( &jpb->jvm_version, javaDict ); 496 setJVMVersion( jpb->jvm_version ); 497 498 // 499 // Set-up the JVM initialization options. 500 // 501 jpb->jvm_args.version = JNI_VERSION_1_4; 502 jpb->jvm_args.nOptions = getJVMOptions( &jpb->jvm_args.options, javaDict ); 503 jpb->jvm_args.ignoreUnrecognized = JNI_TRUE; 504 505 // 506 // We need to replace any -Xmx option that may have been specified in the 507 // Info.plist file with one generated from the user preference. 508 // 509 replaceXmxOption( &jpb->jvm_args ); 510 511 // 512 // Set-up the main class name and main()'s arguments. 513 // 514 getMainClassName( &jpb->main_className, javaDict ); 515 jpb->main_argc = getMainArgs( &jpb->main_argv, javaDict ); 516} 517 518/* vim:set et sw=4 ts=4: */ 519