1 /******************************************************************************* 2 * Copyright 2011 See AUTHORS file. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 ******************************************************************************/ 16 17 package com.badlogic.gdx.jnigen; 18 19 import java.io.InputStream; 20 import java.nio.Buffer; 21 import java.util.ArrayList; 22 23 import com.badlogic.gdx.jnigen.parsing.CMethodParser; 24 import com.badlogic.gdx.jnigen.parsing.CMethodParser.CMethod; 25 import com.badlogic.gdx.jnigen.parsing.CMethodParser.CMethodParserResult; 26 import com.badlogic.gdx.jnigen.parsing.JavaMethodParser; 27 import com.badlogic.gdx.jnigen.parsing.JavaMethodParser.Argument; 28 import com.badlogic.gdx.jnigen.parsing.JavaMethodParser.JavaMethod; 29 import com.badlogic.gdx.jnigen.parsing.JavaMethodParser.JavaSegment; 30 import com.badlogic.gdx.jnigen.parsing.JavaMethodParser.JniSection; 31 import com.badlogic.gdx.jnigen.parsing.JniHeaderCMethodParser; 32 import com.badlogic.gdx.jnigen.parsing.RobustJavaMethodParser; 33 34 /** Goes through a Java source directory, checks each .java file for native methods and emits C/C++ code accordingly, both .h and 35 * .cpp files. 36 * 37 * <h2>Augmenting Java Files with C/C++</h2> C/C++ code can be directly added to native methods in the Java file as block comments 38 * starting at the same line as the method signature. Custom JNI code that is not associated with a native method can be added via 39 * a special block comment as shown below.</p> 40 * 41 * All arguments can be accessed by the name specified in the Java native method signature (unless you use $ in your identifier 42 * which is allowed in Java). 43 * 44 * <pre> 45 * package com.badlogic.jnigen; 46 * 47 * public class MyJniClass { 48 * /*JNI 49 * #include <math.h> 50 * *<i>/</i> 51 * 52 * public native void addToArray(float[] array, int len, float value); /* 53 * for(int i = 0; i < len; i++) { 54 * array[i] = value; 55 * } 56 * *<i>/</i> 57 * } 58 * </pre> 59 * 60 * The generated header file is automatically included in the .cpp file. Methods and custom JNI code can be mixed throughout the 61 * Java file, their order is preserved in the generated .cpp file. Method overloading is supported but not recommended as the 62 * overloading detection is very basic.</p> 63 * 64 * If a native method has strings, one dimensional primitive arrays or direct {@link Buffer} instances as arguments, JNI setup and 65 * cleanup code is automatically generated.</p> 66 * 67 * The following list gives the mapping from Java to C/C++ types for arguments: 68 * 69 * <table border="1"> 70 * <tr> 71 * <td>Java</td> 72 * <td>C/C++</td> 73 * </tr> 74 * <tr> 75 * <td>String</td> 76 * <td>char* (UTF-8)</td> 77 * </tr> 78 * <tr> 79 * <td>boolean[]</td> 80 * <td>bool*</td> 81 * </tr> 82 * <tr> 83 * <td>byte[]</td> 84 * <td>char*</td> 85 * </tr> 86 * <tr> 87 * <td>char[]</td> 88 * <td>unsigned short*</td> 89 * </tr> 90 * <tr> 91 * <td>short[]</td> 92 * <td>short*</td> 93 * </tr> 94 * <tr> 95 * <td>int[]</td> 96 * <td>int*</td> 97 * </tr> 98 * <tr> 99 * <td>long[]</td> 100 * <td>long long*</td> 101 * </tr> 102 * <tr> 103 * <td>float[]</td> 104 * <td>float*</td> 105 * </tr> 106 * <tr> 107 * <td>double[]</td> 108 * <td>double*</td> 109 * </tr> 110 * <tr> 111 * <td>Buffer</td> 112 * <td>unsigned char*</td> 113 * </tr> 114 * <tr> 115 * <td>ByteBuffer</td> 116 * <td>char*</td> 117 * </tr> 118 * <tr> 119 * <td>CharBuffer</td> 120 * <td>unsigned short*</td> 121 * </tr> 122 * <tr> 123 * <td>ShortBuffer</td> 124 * <td>short*</td> 125 * </tr> 126 * <tr> 127 * <td>IntBuffer</td> 128 * <td>int*</td> 129 * </tr> 130 * <tr> 131 * <td>LongBuffer</td> 132 * <td>long long*</td> 133 * </tr> 134 * <tr> 135 * <td>FloatBuffer</td> 136 * <td>float*</td> 137 * </tr> 138 * <tr> 139 * <td>DoubleBuffer</td> 140 * <td>double*</td> 141 * </tr> 142 * <tr> 143 * <td>Anything else</td> 144 * <td>jobject/jobjectArray</td> 145 * </tr> 146 * </table> 147 * 148 * If you need control over setting up and cleaning up arrays/strings and direct buffers you can tell the NativeCodeGenerator to 149 * omit setup and cleanup code by starting the native code block comment with "/*MANUAL" instead of just "/*" to the method name. 150 * See libgdx's Gdx2DPixmap load() method for an example. 151 * 152 * <h2>.h/.cpp File Generation</h2> The .h files are created via javah, which has to be on your path. The Java classes have to be 153 * compiled and accessible to the javah tool. The name of the generated .h/.cpp files is the fully qualified name of the class, 154 * e.g. com.badlogic.jnigen.MyJniClass.h/.cpp. The generator takes the following parameters as input: 155 * 156 * <ul> 157 * <li>Java source directory, containing the .java files, e.g. src/ in an Eclipse project</li> 158 * <li>Java class directory, containing the compiled .class files, e.g. bin/ in an Eclipse project</li> 159 * <li>JNI output directory, where the resulting .h and .cpp files will be stored, e.g. jni/</li> 160 * </ul> 161 * 162 * A default invocation of the generator looks like this: 163 * 164 * <pre> 165 * new NativeCodeGenerator().generate("src", "bin", "jni"); 166 * </pre> 167 * 168 * To automatically compile and load the native code, see the classes {@link AntScriptGenerator}, {@link BuildExecutor} and 169 * {@link JniGenSharedLibraryLoader} classes. </p> 170 * 171 * @author mzechner */ 172 public class NativeCodeGenerator { 173 private static final String JNI_METHOD_MARKER = "native"; 174 private static final String JNI_ARG_PREFIX = "obj_"; 175 private static final String JNI_RETURN_VALUE = "JNI_returnValue"; 176 private static final String JNI_WRAPPER_PREFIX = "wrapped_"; 177 FileDescriptor sourceDir; 178 String classpath; 179 FileDescriptor jniDir; 180 String[] includes; 181 String[] excludes; 182 AntPathMatcher matcher = new AntPathMatcher(); 183 JavaMethodParser javaMethodParser = new RobustJavaMethodParser(); 184 CMethodParser cMethodParser = new JniHeaderCMethodParser(); 185 CMethodParserResult cResult; 186 187 /** Generates .h/.cpp files from the Java files found in "src/", with their .class files being in "bin/". The generated files 188 * will be stored in "jni/". All paths are relative to the applications working directory. 189 * @throws Exception */ 190 public void generate () throws Exception { 191 generate("src", "bin", "jni", null, null); 192 } 193 194 /** Generates .h/.cpp fiels from the Java files found in <code>sourceDir</code>, with their .class files being in 195 * <code>classpath</code>. The generated files will be stored in <code>jniDir</code>. All paths are relative to the 196 * applications working directory. 197 * @param sourceDir the directory containing the Java files 198 * @param classpath the directory containing the .class files 199 * @param jniDir the output directory 200 * @throws Exception */ 201 public void generate (String sourceDir, String classpath, String jniDir) throws Exception { 202 generate(sourceDir, classpath, jniDir, null, null); 203 } 204 205 /** Generates .h/.cpp fiels from the Java files found in <code>sourceDir</code>, with their .class files being in 206 * <code>classpath</code>. The generated files will be stored in <code>jniDir</code>. The <code>includes</code> and 207 * <code>excludes</code> parameters allow to specify directories and files that should be included/excluded from the 208 * generation. These can be given in the Ant path format. All paths are relative to the applications working directory. 209 * @param sourceDir the directory containing the Java files 210 * @param classpath the directory containing the .class files 211 * @param jniDir the output directory 212 * @param includes files/directories to include, can be null (all files are used) 213 * @param excludes files/directories to exclude, can be null (no files are excluded) 214 * @throws Exception */ 215 public void generate (String sourceDir, String classpath, String jniDir, String[] includes, String[] excludes) 216 throws Exception { 217 this.sourceDir = new FileDescriptor(sourceDir); 218 this.jniDir = new FileDescriptor(jniDir); 219 this.classpath = classpath; 220 this.includes = includes; 221 this.excludes = excludes; 222 223 // check if source directory exists 224 if (!this.sourceDir.exists()) { 225 throw new Exception("Java source directory '" + sourceDir + "' does not exist"); 226 } 227 228 // generate jni directory if necessary 229 if (!this.jniDir.exists()) { 230 if (!this.jniDir.mkdirs()) { 231 throw new Exception("Couldn't create JNI directory '" + jniDir + "'"); 232 } 233 } 234 235 // process the source directory, emitting c/c++ files to jniDir 236 processDirectory(this.sourceDir); 237 } 238 239 private void processDirectory (FileDescriptor dir) throws Exception { 240 FileDescriptor[] files = dir.list(); 241 for (FileDescriptor file : files) { 242 if (file.isDirectory()) { 243 if (file.path().contains(".svn")) continue; 244 if (excludes != null && matcher.match(file.path(), excludes)) continue; 245 processDirectory(file); 246 } else { 247 if (file.extension().equals("java")) { 248 if (file.name().contains("NativeCodeGenerator")) continue; 249 if (includes != null && !matcher.match(file.path(), includes)) continue; 250 if (excludes != null && matcher.match(file.path(), excludes)) continue; 251 String className = getFullyQualifiedClassName(file); 252 FileDescriptor hFile = new FileDescriptor(jniDir.path() + "/" + className + ".h"); 253 FileDescriptor cppFile = new FileDescriptor(jniDir + "/" + className + ".cpp"); 254 if (file.lastModified() < cppFile.lastModified()) { 255 System.out.println("C/C++ for '" + file.path() + "' up to date"); 256 continue; 257 } 258 String javaContent = file.readString(); 259 if (javaContent.contains(JNI_METHOD_MARKER)) { 260 ArrayList<JavaSegment> javaSegments = javaMethodParser.parse(javaContent); 261 if (javaSegments.size() == 0) { 262 System.out.println("Skipping '" + file + "', no JNI code found."); 263 continue; 264 } 265 System.out.print("Generating C/C++ for '" + file + "'..."); 266 generateHFile(file); 267 generateCppFile(javaSegments, hFile, cppFile); 268 System.out.println("done"); 269 } 270 } 271 } 272 } 273 } 274 275 private String getFullyQualifiedClassName (FileDescriptor file) { 276 String className = file.path().replace(sourceDir.path(), "").replace('\\', '.').replace('/', '.').replace(".java", ""); 277 if (className.startsWith(".")) className = className.substring(1); 278 return className; 279 } 280 281 private void generateHFile (FileDescriptor file) throws Exception { 282 String className = getFullyQualifiedClassName(file); 283 String command = "javah -classpath " + classpath + " -o " + jniDir.path() + "/" + className + ".h " + className; 284 Process process = Runtime.getRuntime().exec(command); 285 process.waitFor(); 286 if (process.exitValue() != 0) { 287 System.out.println(); 288 System.out.println("Command: " + command); 289 InputStream errorStream = process.getErrorStream(); 290 int c = 0; 291 while ((c = errorStream.read()) != -1) { 292 System.out.print((char)c); 293 } 294 } 295 } 296 297 protected void emitHeaderInclude (StringBuffer buffer, String fileName) { 298 buffer.append("#include <" + fileName + ">\n"); 299 } 300 301 private void generateCppFile (ArrayList<JavaSegment> javaSegments, FileDescriptor hFile, FileDescriptor cppFile) 302 throws Exception { 303 String headerFileContent = hFile.readString(); 304 ArrayList<CMethod> cMethods = cMethodParser.parse(headerFileContent).getMethods(); 305 306 StringBuffer buffer = new StringBuffer(); 307 emitHeaderInclude(buffer, hFile.name()); 308 309 for (JavaSegment segment : javaSegments) { 310 if (segment instanceof JniSection) { 311 emitJniSection(buffer, (JniSection)segment); 312 } 313 314 if (segment instanceof JavaMethod) { 315 JavaMethod javaMethod = (JavaMethod)segment; 316 if (javaMethod.getNativeCode() == null) { 317 throw new RuntimeException("Method '" + javaMethod.getName() + "' has no body"); 318 } 319 CMethod cMethod = findCMethod(javaMethod, cMethods); 320 if (cMethod == null) 321 throw new RuntimeException("Couldn't find C method for Java method '" + javaMethod.getClassName() + "#" 322 + javaMethod.getName() + "'"); 323 emitJavaMethod(buffer, javaMethod, cMethod); 324 } 325 } 326 cppFile.writeString(buffer.toString(), false, "UTF-8"); 327 } 328 329 private CMethod findCMethod (JavaMethod javaMethod, ArrayList<CMethod> cMethods) { 330 for (CMethod cMethod : cMethods) { 331 String javaMethodName = javaMethod.getName().replace("_", "_1"); 332 String javaClassName = javaMethod.getClassName().toString().replace("_", "_1"); 333 if (cMethod.getHead().endsWith(javaClassName + "_" + javaMethodName) 334 || cMethod.getHead().contains(javaClassName + "_" + javaMethodName + "__")) { 335 // FIXME poor man's overloaded method check... 336 // FIXME float test[] won't work, needs to be float[] test. 337 if (cMethod.getArgumentTypes().length - 2 == javaMethod.getArguments().size()) { 338 boolean match = true; 339 for (int i = 2; i < cMethod.getArgumentTypes().length; i++) { 340 String cType = cMethod.getArgumentTypes()[i]; 341 String javaType = javaMethod.getArguments().get(i - 2).getType().getJniType(); 342 if (!cType.equals(javaType)) { 343 match = false; 344 break; 345 } 346 } 347 348 if (match) { 349 return cMethod; 350 } 351 } 352 } 353 } 354 return null; 355 } 356 357 private void emitLineMarker (StringBuffer buffer, int line) { 358 buffer.append("\n//@line:"); 359 buffer.append(line); 360 buffer.append("\n"); 361 } 362 363 private void emitJniSection (StringBuffer buffer, JniSection section) { 364 emitLineMarker(buffer, section.getStartIndex()); 365 buffer.append(section.getNativeCode().replace("\r", "")); 366 } 367 368 private void emitJavaMethod (StringBuffer buffer, JavaMethod javaMethod, CMethod cMethod) { 369 // get the setup and cleanup code for arrays, buffers and strings 370 StringBuffer jniSetupCode = new StringBuffer(); 371 StringBuffer jniCleanupCode = new StringBuffer(); 372 StringBuffer additionalArgs = new StringBuffer(); 373 StringBuffer wrapperArgs = new StringBuffer(); 374 emitJniSetupCode(jniSetupCode, javaMethod, additionalArgs, wrapperArgs); 375 emitJniCleanupCode(jniCleanupCode, javaMethod, cMethod); 376 377 // check if the user wants to do manual setup of JNI args 378 boolean isManual = javaMethod.isManual(); 379 380 // if we have disposable arguments (string, buffer, array) and if there is a return 381 // in the native code (conservative, not syntactically checked), emit a wrapper method. 382 if (javaMethod.hasDisposableArgument() && javaMethod.getNativeCode().contains("return")) { 383 // if the method is marked as manual, we just emit the signature and let the 384 // user do whatever she wants. 385 if (isManual) { 386 emitMethodSignature(buffer, javaMethod, cMethod, null, false); 387 emitMethodBody(buffer, javaMethod); 388 buffer.append("}\n\n"); 389 } else { 390 // emit the method containing the actual code, called by the wrapper 391 // method with setup pointers to arrays, buffers and strings 392 String wrappedMethodName = emitMethodSignature(buffer, javaMethod, cMethod, additionalArgs.toString()); 393 emitMethodBody(buffer, javaMethod); 394 buffer.append("}\n\n"); 395 396 // emit the wrapper method, the one with the declaration in the header file 397 emitMethodSignature(buffer, javaMethod, cMethod, null); 398 if (!isManual) { 399 buffer.append(jniSetupCode); 400 } 401 402 if (cMethod.getReturnType().equals("void")) { 403 buffer.append("\t" + wrappedMethodName + "(" + wrapperArgs.toString() + ");\n\n"); 404 if (!isManual) { 405 buffer.append(jniCleanupCode); 406 } 407 buffer.append("\treturn;\n"); 408 } else { 409 buffer.append("\t" + cMethod.getReturnType() + " " + JNI_RETURN_VALUE + " = " + wrappedMethodName + "(" 410 + wrapperArgs.toString() + ");\n\n"); 411 if (!isManual) { 412 buffer.append(jniCleanupCode); 413 } 414 buffer.append("\treturn " + JNI_RETURN_VALUE + ";\n"); 415 } 416 buffer.append("}\n\n"); 417 } 418 } else { 419 emitMethodSignature(buffer, javaMethod, cMethod, null); 420 if (!isManual) { 421 buffer.append(jniSetupCode); 422 } 423 emitMethodBody(buffer, javaMethod); 424 if (!isManual) { 425 buffer.append(jniCleanupCode); 426 } 427 buffer.append("}\n\n"); 428 } 429 430 } 431 432 protected void emitMethodBody (StringBuffer buffer, JavaMethod javaMethod) { 433 // emit a line marker 434 emitLineMarker(buffer, javaMethod.getEndIndex()); 435 436 // FIXME add tabs cleanup 437 buffer.append(javaMethod.getNativeCode()); 438 buffer.append("\n"); 439 } 440 441 private String emitMethodSignature (StringBuffer buffer, JavaMethod javaMethod, CMethod cMethod, String additionalArguments) { 442 return emitMethodSignature(buffer, javaMethod, cMethod, additionalArguments, true); 443 } 444 445 private String emitMethodSignature (StringBuffer buffer, JavaMethod javaMethod, CMethod cMethod, String additionalArguments, 446 boolean appendPrefix) { 447 // emit head, consisting of JNIEXPORT,return type and method name 448 // if this is a wrapped method, prefix the method name 449 String wrappedMethodName = null; 450 if (additionalArguments != null) { 451 String[] tokens = cMethod.getHead().replace("\r\n", "").replace("\n", "").split(" "); 452 wrappedMethodName = JNI_WRAPPER_PREFIX + tokens[3]; 453 buffer.append("static inline "); 454 buffer.append(tokens[1]); 455 buffer.append(" "); 456 buffer.append(wrappedMethodName); 457 buffer.append("\n"); 458 } else { 459 buffer.append(cMethod.getHead()); 460 } 461 462 // construct argument list 463 // Differentiate between static and instance method, then output each argument 464 if (javaMethod.isStatic()) { 465 buffer.append("(JNIEnv* env, jclass clazz"); 466 } else { 467 buffer.append("(JNIEnv* env, jobject object"); 468 } 469 if (javaMethod.getArguments().size() > 0) buffer.append(", "); 470 for (int i = 0; i < javaMethod.getArguments().size(); i++) { 471 // output the argument type as defined in the header 472 buffer.append(cMethod.getArgumentTypes()[i + 2]); 473 buffer.append(" "); 474 // if this is not a POD or an object, we need to add a prefix 475 // as we will output JNI code to get pointers to strings, arrays 476 // and direct buffers. 477 Argument javaArg = javaMethod.getArguments().get(i); 478 if (!javaArg.getType().isPlainOldDataType() && !javaArg.getType().isObject() && appendPrefix) { 479 buffer.append(JNI_ARG_PREFIX); 480 } 481 // output the name of the argument 482 buffer.append(javaArg.getName()); 483 484 // comma, if this is not the last argument 485 if (i < javaMethod.getArguments().size() - 1) buffer.append(", "); 486 } 487 488 // if this is a wrapper method signature, add the additional arguments 489 if (additionalArguments != null) { 490 buffer.append(additionalArguments); 491 } 492 493 // close signature, open method body 494 buffer.append(") {\n"); 495 496 // return the wrapped method name if any 497 return wrappedMethodName; 498 } 499 500 private void emitJniSetupCode (StringBuffer buffer, JavaMethod javaMethod, StringBuffer additionalArgs, 501 StringBuffer wrapperArgs) { 502 // add environment and class/object as the two first arguments for 503 // wrapped method. 504 if (javaMethod.isStatic()) { 505 wrapperArgs.append("env, clazz, "); 506 } else { 507 wrapperArgs.append("env, object, "); 508 } 509 510 // arguments for wrapper method 511 for (int i = 0; i < javaMethod.getArguments().size(); i++) { 512 Argument arg = javaMethod.getArguments().get(i); 513 if (!arg.getType().isPlainOldDataType() && !arg.getType().isObject()) { 514 wrapperArgs.append(JNI_ARG_PREFIX); 515 } 516 // output the name of the argument 517 wrapperArgs.append(arg.getName()); 518 if (i < javaMethod.getArguments().size() - 1) wrapperArgs.append(", "); 519 } 520 521 // direct buffer pointers 522 for (Argument arg : javaMethod.getArguments()) { 523 if (arg.getType().isBuffer()) { 524 String type = arg.getType().getBufferCType(); 525 buffer.append("\t" + type + " " + arg.getName() + " = (" + type + ")(" + JNI_ARG_PREFIX + arg.getName() 526 + "?env->GetDirectBufferAddress(" + JNI_ARG_PREFIX + arg.getName() + "):0);\n"); 527 additionalArgs.append(", "); 528 additionalArgs.append(type); 529 additionalArgs.append(" "); 530 additionalArgs.append(arg.getName()); 531 wrapperArgs.append(", "); 532 wrapperArgs.append(arg.getName()); 533 } 534 } 535 536 // string pointers 537 for (Argument arg : javaMethod.getArguments()) { 538 if (arg.getType().isString()) { 539 String type = "char*"; 540 buffer.append("\t" + type + " " + arg.getName() + " = (" + type + ")env->GetStringUTFChars(" + JNI_ARG_PREFIX 541 + arg.getName() + ", 0);\n"); 542 additionalArgs.append(", "); 543 additionalArgs.append(type); 544 additionalArgs.append(" "); 545 additionalArgs.append(arg.getName()); 546 wrapperArgs.append(", "); 547 wrapperArgs.append(arg.getName()); 548 } 549 } 550 551 // Array pointers, we have to collect those last as GetPrimitiveArrayCritical 552 // will explode into our face if we call another JNI method after that. 553 for (Argument arg : javaMethod.getArguments()) { 554 if (arg.getType().isPrimitiveArray()) { 555 String type = arg.getType().getArrayCType(); 556 buffer.append("\t" + type + " " + arg.getName() + " = (" + type + ")env->GetPrimitiveArrayCritical(" + JNI_ARG_PREFIX 557 + arg.getName() + ", 0);\n"); 558 additionalArgs.append(", "); 559 additionalArgs.append(type); 560 additionalArgs.append(" "); 561 additionalArgs.append(arg.getName()); 562 wrapperArgs.append(", "); 563 wrapperArgs.append(arg.getName()); 564 } 565 } 566 567 // new line for separation 568 buffer.append("\n"); 569 } 570 571 private void emitJniCleanupCode (StringBuffer buffer, JavaMethod javaMethod, CMethod cMethod) { 572 // emit cleanup code for arrays, must come first 573 for (Argument arg : javaMethod.getArguments()) { 574 if (arg.getType().isPrimitiveArray()) { 575 buffer.append("\tenv->ReleasePrimitiveArrayCritical(" + JNI_ARG_PREFIX + arg.getName() + ", " + arg.getName() 576 + ", 0);\n"); 577 } 578 } 579 580 // emit cleanup code for strings 581 for (Argument arg : javaMethod.getArguments()) { 582 if (arg.getType().isString()) { 583 buffer.append("\tenv->ReleaseStringUTFChars(" + JNI_ARG_PREFIX + arg.getName() + ", " + arg.getName() + ");\n"); 584 } 585 } 586 587 // new line for separation 588 buffer.append("\n"); 589 } 590 } 591