Rhino on Android

2,599 views
Skip to first unread message

Hannes Wallnoefer

unread,
Jan 19, 2012, 10:27:49 AM1/19/12
to mozilla-rhino
I've recently started experimenting with Rhino on Android and was
quite pleasantly surprised by the result.

Startup time is almost instantaneous, and although Rhino runs in
interpreted mode it seem quite fast (indistinguishable from Java for
typical plumbing style application code). And although JavaAdapter is
not available (because of Android's DEX bytecode format) implementing
interfaces via InterfaceAdapter/Proxy is and fits in nicely with
Android's API (which contains lots of single-method interfaces).

So the downside is that one has to manually subclass Android classes
to allow methods to be implemented in JavaScript. There are options
for making this easier in the future like providing ready-made
subclasses to common Android components or providing a tool to
generate subclasses like Ruboto does. For now my solution is to try to
make subclassing as simple and elegant as possible - just a few lines
of code to let a class register and invoke script callbacks.

The demo app I've been messing with is available here:
https://github.com/hns/jsdroid-demo

I plan to evolve this into a proper project over the next days/weeks.
In case you're interested I just created a google group:
http://groups.google.com/group/jsdroid

You're welcome to join and contribute ideas and code. I'm especially
interested in hearing from people with an Android development
background since I'm a complete newbie here - this is the very first
Android code I've ever written.

Hannes

Tony Zakula

unread,
Jan 20, 2012, 10:24:26 AM1/20/12
to mozill...@googlegroups.com
Hi Hannes,

It is great to see more interest for Rhino on mobile platforms. We
use it at Expensify.com extensively. Here is a link to a blog post
with a few details about Rhino on Android -
http://blog.expensify.com/2011/08/30/expensify-hiring-a-net-programmer-seriously

Our market app -
https://market.android.com/details?id=org.me.mobiexpensifyg&hl=en

Currently, our cross platform layer covers Android, BlackBerry,
iPhone, and Windows Phone with more platforms planned. We use Rhino
on Android and BlackBerry.

I have joined the other list, and I will be happy to contribute in any
way I can.

Thanks!

Tony Z

Perry

unread,
Jan 22, 2012, 7:49:43 PM1/22/12
to mozilla-rhino
I have rhino embedded in one of my android applications (recontrolr --
https://recontrolr.appspot.com/) -- it works great.

Only being able to use JavaAdapter and implementing only interfaces
kinda sucks, but it is what it is without codegen.

Hannes Wallnoefer

unread,
Jan 25, 2012, 3:20:13 AM1/25/12
to mozilla-rhino
On Jan 23, 1:49 am, Perry <pfngu...@gmail.com> wrote:
> I have rhino embedded in one of my android applications (recontrolr --https://recontrolr.appspot.com/) -- it works great.
>
> Only being able to use JavaAdapter and implementing only interfaces
> kinda sucks, but it is what it is without codegen.


Yes, that's a drawback. I've tried DexMaker which is a very new
project to generate DEX bytecode on the fly, but as cool as it is I
found it too slow for real world use (several seconds to generate a
class).

http://code.google.com/p/dexmaker/

What I've done now is to make InterfaceAdapter (which is usable on
Android as it's based on java.lang.reflect.Proxy) more useful.
Previously it could only implement single-method interfaces by passing
a function to a Java method expecting that interface.

Now it's also possible to implement an interface with multiple methods
by passing a JS object, and on a Dalvik VM using a Java interface as
constructor as in `new org.package.MyInterface(funcOrObj)` will create
an InterfaceAdapter instead of a JavaAdapter.

https://github.com/mozilla/rhino/commit/3e83f18588cda4fe6701ba388afd3d99b5849d53
https://github.com/mozilla/rhino/commit/c2366ca9b2e1a13362bd4c7a5054206b55aba792

I've pushed these changes to the master and rhino_1_8 branches.

Hannes

Frank P. Westlake

unread,
Jan 26, 2012, 11:29:53 AM1/26/12
to mozill...@googlegroups.com
On 11:59, Hannes Wallnoefer wrote:
> Now it's also possible to implement an interface with multiple methods
> by passing a JS object, and on a Dalvik VM using a Java interface as
> constructor as in `new org.package.MyInterface(funcOrObj)` will create
> an InterfaceAdapter instead of a JavaAdapter.

Thank you for all your work Hannes, I appreciate this addition.

Rhino 1.7 release 4 PRERELEASE pulled after this message.

I've tried your new adapter and I have had to make some changes from how
I use my own adapter and I don't know it these changes are because I
have been doing things wrongly and am now needing to do them rightly, or
if they are because something is wrong in the new adapter.

There are three things discussed below:

1. Scope of 'this' with respect to methods.
2. Additional method parameters in 'arguments'.
3. Data type of numbers now defaulting to string.

All of these are in the script below. This is not a script I need and I
am using 'java.io.DataInput()' only to test the interface.

1. Scope of 'this' with respect to methods.
In my proxy/adapter the 'this' of the interface is retained within it's
methods. In the new adapter you have provided it is different. I am not
sure but it seems to be the script's 'this'.

2. Additional method parameters in 'arguments'.
With the new adapter it seems that a method 'this.method(a, b, c)' is
given 'arguments=["a", "b", "c", "method"]'. I think you have provided
this so that a method could know how it was called. Should we always
expect the additional argument?

3. Data type of numbers now defaulting to string.
With my adapter 'length+=offset;' performed the mathematical operation.
In the new adapter it does that for the first call ('0' += '5' == '05')
only because 05==5, but in the second the string concatenation ('5' +=
'11' == '511') causes an error.

// JS SCRIPT
var di=java.io.DataInput(
new function()
{
// ADDED 1 LINE.
var THAT=this;
this.s="Rhino JavaScript";
this.skipBytes= function(n) {return(n);}
this.readBoolean= function() {return(true);}
this.readByte= function() {return(0x32);}
this.readUnsignedByte= function() {return(1<<5|0x12);}
this.readDouble= function() {return(3.3*1.0);}
this.readFloat= function() {return(4.4/1.0);}
this.readLong= function() {return(5);}
this.readInt= function() {return(6);}
this.readShort= function() {return(7);}
this.readUnsignedShort= function() {return(7);}
this.readChar= function() {return("8");}
this.readLine= function() {return("9");}
this.readUTF= function() {return("10");}
this.readFully= function(array, offset, length)
{
// ORIGINAL, DISABLED 4 LINES.
// if(arguments.length<2) offset=0;
// if(arguments.length<3) length=array.length;
// length+=offset;
// for(; offset<length; offset++)
array[offset]=this.s.charCodeAt(offset);

// ADDED 2 LINES.
if(offset==undefined) {offset=0; length=array.length;}
for(var n=1*offset+1*length; offset<n; offset++)
array[offset]=THAT.s.charCodeAt(offset);
// DISABLE ABOVE 2 AND ENABLE BELOW 1 TO SEE THE OPERATION.
output(offset+" "+length, offset+=length);
}
}
);
var B=java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, 16);
di.readFully(B, 0, 5);
di.readFully(B, 5, 11);
output("readFully", java.lang.String(B));
output("skipBytes", di.skipBytes(0));
output("readBoolean", di.readBoolean());
var b=java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, 1);
b[0]=di.readByte();
output("readByte (array)", java.lang.String(b));
output("readByte (raw)", di.readByte());
output("readUnsignedByte", di.readUnsignedByte());
output("readDouble", di.readDouble());
output("readFloat", di.readFloat());
output("readLong", di.readLong());
output("readInt", di.readInt());
output("readShort", di.readShort());
output("readUnsignedShort", di.readUnsignedShort());
var c=java.lang.reflect.Array.newInstance(java.lang.Character.TYPE, 1);
c[0]=di.readChar();
output("readChar (array)", java.lang.String(c));
output("readChar (raw)", di.readChar());
output("readLine", di.readLine());
output("getClass", di.getClass());
output("readUTF", di.readUTF());
function output(f, s)
{
print(f+"=("+s+") ");
}
// END

//Frank

Hannes Wallnöfer

unread,
Jan 27, 2012, 5:35:29 AM1/27/12
to mozill...@googlegroups.com
Thanks for the feedback, Frank!

These were all quirks that were already in InterfaceAdapter before the
changes I did. Should be fixed now in master and rhino_1_8.

Hannes

2012/1/26 Frank P. Westlake <frank.w...@gmail.com>:

Frank P. Westlake

unread,
Jan 27, 2012, 10:29:46 AM1/27/12
to mozill...@googlegroups.com
On 2012-01-27 02:35, Hannes Walln�fer wrote:
> These were all quirks that were already in InterfaceAdapter before the
> changes I did. Should be fixed now in master and rhino_1_8.

Thank you Hannes. I read my message again on 'this' and I see that it
was a little confusing, about like my code. Your changes work well but
'this' still requires 'THAT' (huh?). After examining it again I see
'this.s.charCodeAt(offset)' should not succeed because 's' is not
defined by 'DataInput'. I checked the code in my own adapter and I see
that it succeeds there because I assume that if it isn't a method then
it must be a field:

if(fObj instanceof Function)
result=F.call(cx, mJavaScript, thisObject, jsArgs);
else result=mJavaScript.get(method.getName(), thisObject);

I don't know if that is proper or not but it is useful so I think I'll
leave it in.

Thanks again Hannes -- please don't retire anytime soon!

Frank

Frank P. Westlake

unread,
Jan 28, 2012, 8:51:59 AM1/28/12
to mozill...@googlegroups.com
Here's another change:

var dir=java.io.File("/lib");
var list=dir.list(
function(f, s)
{
// /*1*/ return(s.endsWith(".so"));
/*2*/ return(java.lang.String(s).endsWith(".so"));
}
);
for(var a in list) print(list[a]);

Line /*1*/ is what I used prior to the new adapter and line /*2*/ is
what must be used now.

Frank

Hannes Wallnöfer

unread,
Jan 30, 2012, 4:26:49 AM1/30/12
to mozill...@googlegroups.com
Wait - that's a direct consequence of the last commit I made on your
request, right? Because it means that previously you expected String
arguments to be delivered as wrapped java.lang.String instances,
whereas now you get native JS strings (represented by unwrapped java
strings).

Hannes

2012/1/28 Frank P. Westlake <frank.w...@gmail.com>:

Frank P. Westlake

unread,
Jan 30, 2012, 9:31:42 AM1/30/12
to mozill...@googlegroups.com
On 2012-01-30 01:26, Hannes Walln�fer wrote:
> Wait - that's a direct consequence of the last commit I made on your
> request, right?

Yes. After you changed the interface adapter so that numbers do not
default to strings and so that 'arguments.length' reflects the number of
specified parameters, I pulled and built and pumped and pushed and
tested and was happy. Later with that build I noticed this change with
the object-type interface.

> Because it means that previously you expected String
> arguments to be delivered as wrapped java.lang.String instances,
> whereas now you get native JS strings (represented by unwrapped java
> strings).

Well I don't know what I expect, which is why I call them changes
instead of bugs or errors. I'm bringing them to your attention just in
case they are something that should be brought to your attention. I will
make do with whichever you say is correct.

Frank

Reply all
Reply to author
Forward
0 new messages