-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a JavaScript REPL provided by Mozilla's Rhino
This is a first draft at adding a REPL. What works: - basic JS expressions - evaluatied statements preserve their states (can create a variable and it will persist between successive calls) - return types are properly expanded by chrome's inspector - binding of android's Context (passed as `context`) - binding for classes: `System.gc()` is `Packages.java.lang.System.gc()` - console redirection with `console.log()` - `importPackage()` to import all classes under a java package - `importClass()` to import a single class - binding variables - binding functions For more details see: - https://developer.mozilla.org/en-US/docs/Mozilla/Projects/Rhino/Scripting_Java - https://developer.mozilla.org/en-US/docs/Rhino_documentation
- Loading branch information
Showing
9 changed files
with
684 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
include ':stetho' | ||
include ':stetho-urlconnection' | ||
include ':stetho-okhttp' | ||
include ':stetho-js-rhino' | ||
include ':stetho-sample' | ||
include ':stetho-timber' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
/build |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,224 @@ | ||
# Stetho's JavaScript Module | ||
|
||
This [Stetho](https://facebook.github.io/stetho) plugin adds a JavaScript console by embedding Mozilla's [Rhino](https://github.com/mozilla/rhino). | ||
|
||
## Set-up | ||
|
||
### Download | ||
Download [the latest JARs](https://github.com/facebook/stetho/releases/latest) or grab via Gradle: | ||
```groovy | ||
compile 'com.facebook.stetho:stetho-js-rhino:1.1.1' | ||
``` | ||
or Maven: | ||
```xml | ||
<dependency> | ||
<groupId>com.facebook.stetho</groupId> | ||
<artifactId>stetho-js-rhino</artifactId> | ||
<version>1.1.1</version> | ||
</dependency> | ||
``` | ||
|
||
Make sure that you depend on the main `stetho` dependency too. | ||
|
||
### Putting it together | ||
The JavaScript integration is similar to standard Stetho integration. | ||
The main difference is with the WebKitInspector used. | ||
There is a simple initialization step which occurs in your `Application` class: | ||
|
||
```java | ||
public class MyApplication extends Application { | ||
public void onCreate() { | ||
super.onCreate(); | ||
+ JsRuntimeBuilder jsRuntimeBuilder = new JsRuntimeBuilder(this); | ||
Stetho.initialize( | ||
Stetho.newInitializerBuilder(this) | ||
.enableDumpapp(Stetho.defaultDumperPluginsProvider(this)) | ||
- .enableWebKitInspector(Stetho.defaultInspectorModulesProvider(context)) | ||
+ .enableWebKitInspector(jsRuntimeBuilder.jsInspectorModulesProvider()) | ||
.build()); | ||
} | ||
} | ||
``` | ||
|
||
You can still use other Stetho plugins with this approach, for instance the network helpers stetho-okhttp and stetho-urlconnection will still work if activated properly. | ||
|
||
### How it works | ||
|
||
At the core this plugin initializes a JavaScript runtime provided by Mozilla's [Rhino](https://github.com/mozilla/rhino). | ||
The runtime is configured so that it will work within an Android application. | ||
This means that code has to run in interpreted mode as the more aggressive optimizations performed | ||
by Rhino are done through on-the-fly JVM bytecode generation and this won't work in Android which expects Dalvik bytecode. | ||
|
||
For generic purposes the interpreted mode should have no performance impact since this is debug tool. | ||
|
||
## Customization | ||
|
||
By default a JavaScript interpreter starts with an empty scope (environment) and has no default variables or functions set besides the ones described by the language specification. | ||
|
||
You might be used to the browser setting up various objects for your like `document`, `console`, etc. This is something that's not part of the javascript specification and is particular only | ||
to the browser's runtime. | ||
|
||
## Example | ||
|
||
Once you have enabled the JavaScript console in your app you will be able to run live JavaScript commands on your app from the console. | ||
|
||
Here's an example to show a toast from the console: | ||
|
||
```javascript | ||
importPackage(android.widget); | ||
importPackage(android.os); | ||
var handler = new Handler(Looper.getMainLooper()); | ||
handler.post(function() { Toast.makeText(context, "Hello from JavaScript", Toast.LENGTH_LONG).show() }); | ||
``` | ||
|
||
### Default scope | ||
|
||
Rhino offers the possibility to use an enhanced runtime where some utilities have been added | ||
in order to help the integration with the java runtime. | ||
This is exactly the runtime that the plugin uses. | ||
The default scope used by the plugin is described in more details in the article [Scripting Java](https://developer.mozilla.org/en-US/docs/Mozilla/Projects/Rhino/Scripting_Java). | ||
|
||
The scope can be enhanced by your application, if desired. | ||
You can preload classes and packages, bind variables, objects and even functions. | ||
This means that your java classes and objects can be accessed from JavaScript. | ||
|
||
### Builtins | ||
|
||
The JavaScript runtime use by this plugin has been enhanced. | ||
By default your application's package has been imported. | ||
So things like `R.string.app` should work. | ||
|
||
The functions `importClass` and `importPackage` have been added. | ||
|
||
A `console` object is available too. It supports only a `log()` method for now. | ||
|
||
### Import a class | ||
|
||
To import a java class into the JavaScript runtime do: | ||
|
||
```java | ||
jsRuntimeBuilder.importClass(R.class); | ||
``` | ||
|
||
### Import a package | ||
|
||
To import all classes in a java package into the JavaScript runtime do: | ||
|
||
```java | ||
jsRuntimeBuilder.importPackage("android.content"); | ||
``` | ||
|
||
### Variable binding | ||
|
||
Here's how to pass a variable to the JavaScript runtime: | ||
|
||
```java | ||
jsRuntimeBuilder.addVariable("flag", new AtomicBoolean(true)); | ||
``` | ||
**Note**: Java primitive types will be autoboxed, only objects can be passed to the javascript runtime. | ||
|
||
### Function binding | ||
|
||
You can also add custom javascript functions that will be available to the runtime. | ||
This requires a bit more of work on your part. | ||
Remember that you invoke methods on objects that you have already binded. | ||
|
||
If you really want to define a top level function this is how it can be done: | ||
|
||
```java | ||
// Your application context | ||
final Context context = this; | ||
|
||
final Handler handler = new Handler(Looper.getMainLooper()); | ||
|
||
// Add the function: void toast(String) | ||
jsRuntimeBuilder.addFunction("toast", new BaseFunction() { | ||
@Override | ||
public Object call(org.mozilla.javascript.Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { | ||
// javascript passes the arguments as varags | ||
final String message = args[0].toString(); | ||
handler.post(new Runnable() { | ||
@Override | ||
public void run() { | ||
Toast.makeText(context, message, Toast.LENGTH_LONG).show(); | ||
} | ||
}); | ||
|
||
// return undef in javascript | ||
return org.mozilla.javascript.Context.getUndefinedValue(); | ||
} | ||
}); | ||
``` | ||
|
||
**Note**: This is a complex example since a toast be invoked from the main UI thread. | ||
|
||
## Limitations | ||
|
||
As mentioned in the section (how it works)(#"user-content-how-it-works) the JavaScript runtime has to run in interpreted mode due to the nature of the android runtime. | ||
|
||
### Dex method count | ||
|
||
Rhino is not a small library and it can increase your dex method count by more than 7,000. | ||
The standard Rhino distribution includes a "tools" package that's not required by this plugin | ||
but it is still bundled. | ||
That package alone adds more than 1,200 methods to the dex count. | ||
|
||
Hopefully the Rhino devs will split the distribution in smaller artifacts (see mozilla/rhino#156). | ||
In the meanwhile you should consider using proguard to shrink the dex method count. | ||
|
||
Here's the dex count of the stetho-sample compiled under various scenarios: | ||
|
||
| | Original | JavaScript | | ||
| :--- | -------: | ---------: | | ||
| Dex | 15,461 | 22,749 | | ||
| Size | 1.0M | 1.6M | | ||
|
||
With proguard: | ||
|
||
| | Original | JavaScript | JavaScript (w/o *tools*) | | ||
| :--- | -------: | ---------: | ---------------------------: | | ||
| Dex | 8,013 | 15,316 | 14,043 | | ||
| Size | 847K | 1.5M | 1.4M | | ||
|
||
### Proguard | ||
|
||
To proguard your project add the following rules to your proguard file: | ||
|
||
``` | ||
# stetho | ||
+keep class com.facebook.stetho.** { *; } | ||
# rhino (javascript) | ||
-dontwarn org.mozilla.javascript.** | ||
-dontwarn org.mozilla.classfile.** | ||
-keep class org.mozilla.javascript.** { *; } | ||
``` | ||
|
||
If you want to remove the *tools* package for a more aggressive proguard use: | ||
|
||
``` | ||
# stetho | ||
+keep class com.facebook.stetho.** { *; } | ||
# rhino (javascript) | ||
-dontwarn org.mozilla.javascript.** | ||
-dontwarn org.mozilla.classfile.** | ||
-keep class org.mozilla.classfile.** { *; } | ||
-keep class org.mozilla.javascript.* { *; } | ||
-keep class org.mozilla.javascript.annotations.** { *; } | ||
-keep class org.mozilla.javascript.ast.** { *; } | ||
-keep class org.mozilla.javascript.commonjs.module.** { *; } | ||
-keep class org.mozilla.javascript.commonjs.module.provider.** { *; } | ||
-keep class org.mozilla.javascript.debug.** { *; } | ||
-keep class org.mozilla.javascript.jdk13.** { *; } | ||
-keep class org.mozilla.javascript.jdk15.** { *; } | ||
-keep class org.mozilla.javascript.json.** { *; } | ||
-keep class org.mozilla.javascript.optimizer.** { *; } | ||
-keep class org.mozilla.javascript.regexp.** { *; } | ||
-keep class org.mozilla.javascript.serialize.** { *; } | ||
-keep class org.mozilla.javascript.typedarrays.** { *; } | ||
-keep class org.mozilla.javascript.v8dtoa.** { *; } | ||
-keep class org.mozilla.javascript.xml.** { *; } | ||
-keep class org.mozilla.javascript.xmlimpl.** { *; } | ||
``` | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
apply plugin: 'com.android.library' | ||
|
||
android { | ||
compileSdkVersion 21 | ||
buildToolsVersion "21.1.2" | ||
|
||
defaultConfig { | ||
minSdkVersion 9 | ||
targetSdkVersion 21 | ||
versionCode 1 | ||
versionName "1.0" | ||
} | ||
|
||
lintOptions { | ||
// Rhino has references to awt and swing | ||
disable 'InvalidPackage' | ||
} | ||
} | ||
|
||
dependencies { | ||
compile project(':stetho') | ||
compile 'com.google.code.findbugs:jsr305:2.0.1' | ||
compile 'org.mozilla:rhino:1.7.6' | ||
} | ||
|
||
apply from: rootProject.file('release.gradle') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
POM_NAME=Stetho JavaScript (Rhino) module | ||
POM_ARTIFACT_ID=stetho-js-rhino | ||
POM_OPTIONAL_DEPS=com.facebook.stetho:stetho | ||
POM_PACKAGING=aar |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
<?xml version="1.0" encoding="utf-8"?> | ||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" | ||
package="com.facebook.stetho.rhino"> | ||
|
||
<application /> | ||
|
||
</manifest> |
87 changes: 87 additions & 0 deletions
87
stetho-js-rhino/src/main/java/com/facebook/stetho/rhino/JsConsole.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
/* | ||
* Copyright (c) 2014-present, Facebook, Inc. | ||
* All rights reserved. | ||
* | ||
* This source code is licensed under the BSD-style license found in the | ||
* LICENSE file in the root directory of this source tree. An additional grant | ||
* of patent rights can be found in the PATENTS file in the same directory. | ||
*/ | ||
|
||
package com.facebook.stetho.rhino; | ||
|
||
import android.support.annotation.Nullable; | ||
import com.facebook.stetho.inspector.console.CLog; | ||
import com.facebook.stetho.inspector.protocol.module.Console.MessageLevel; | ||
import com.facebook.stetho.inspector.protocol.module.Console.MessageSource; | ||
import org.mozilla.javascript.Context; | ||
import org.mozilla.javascript.Function; | ||
import org.mozilla.javascript.ScriptRuntime; | ||
import org.mozilla.javascript.Scriptable; | ||
import org.mozilla.javascript.ScriptableObject; | ||
import org.mozilla.javascript.annotations.JSFunction; | ||
|
||
class JsConsole extends ScriptableObject { | ||
|
||
/** | ||
* Serial version UID. | ||
*/ | ||
private static final long serialVersionUID = 1L; | ||
|
||
private @Nullable | ||
JsConsole thePrototypeInstance = null; | ||
|
||
/** | ||
* The zero-parameter constructor. | ||
* | ||
* When Context.defineClass is called with this class, it will construct | ||
* File.prototype using this constructor. | ||
*/ | ||
public JsConsole() { | ||
if (thePrototypeInstance == null) { | ||
thePrototypeInstance = this; | ||
} | ||
} | ||
|
||
public JsConsole(ScriptableObject scope) { | ||
setParentScope(scope); | ||
Object ctor = ScriptRuntime.getTopLevelProp(scope, "Console"); | ||
if (ctor != null && ctor instanceof Scriptable) { | ||
Scriptable scriptable = (Scriptable) ctor; | ||
setPrototype((Scriptable) scriptable.get("prototype", scriptable)); | ||
} | ||
} | ||
|
||
@Override | ||
public String getClassName() { | ||
return "Console"; | ||
} | ||
|
||
@JSFunction | ||
public static void log(Context cx, Scriptable thisObj, Object[] args, Function funObj) { | ||
log(args); | ||
} | ||
|
||
private static void log(Object [] rawArgs) { | ||
String format = (String) rawArgs[0]; | ||
|
||
String message; | ||
if (rawArgs.length == 1) { | ||
message = format; | ||
} | ||
else { | ||
// Using place holders in javascript (%d) causes a problem with java's String.format(). | ||
// This happens because %d expects an int/Integer but in javascript numbers are floats. | ||
// For now as a best effort we just try to use jsToJava() to convert everything to objects. | ||
// This will need rework. | ||
Object [] args = new Object[rawArgs.length - 1]; | ||
for (int i = 0; i < args.length; ++i) { | ||
Object arg = rawArgs[i + 1]; | ||
arg = Context.jsToJava(arg, Object.class); | ||
args[i] = arg; | ||
} | ||
message = String.format(format, args); | ||
} | ||
|
||
CLog.writeToConsole(MessageLevel.LOG, MessageSource.JAVASCRIPT, message); | ||
} | ||
} |
Oops, something went wrong.