JE(3pm) | User Contributed Perl Documentation | JE(3pm) |
JE - Pure-Perl ECMAScript (JavaScript) Engine
Version 0.066 (alpha release)
The API is still subject to change. If you have the time and the interest, please experiment with this module (or even lend a hand :-). If you have any ideas for the API, or would like to help with development, please e-mail the author.
use JE; $j = new JE; # create a new global object $j->eval('({"this": "that", "the": "other"}["this"])'); # returns "that" $parsed = $j->parse('new Array(1,2,3)'); $rv = $parsed->execute; # returns a JE::Object::Array $rv->value; # returns a Perl array ref $obj = $j->eval('new Object'); # create a new object $foo = $j->{document}; # get property $j->{document} = $obj; # set property $j->{document} = {}; # gets converted to a JE::Object $j->{document}{location}{href}; # autovivification $j->method(alert => "text"); # invoke a method # create global function from a Perl subroutine: $j->new_function(print => sub { print @_, "\n" } ); $j->eval(<<'--end--'); function correct(s) { s = s.replace(/[EA]/g, function(s){ return ['E','A'][+(s=='E')] }) return s.charAt(0) + s.substring(1,4).toLowerCase() + s.substring(4) } print(correct("ECMAScript")) // :-) --end--
JE, short for JavaScript::Engine (imaginative, isn't it?), is a pure-Perl JavaScript engine. Here are some of its strengths:
JE's greatest weakness is that it's slow (well, what did you expect?). It also uses and leaks lots of memory. (There is an experimental JE::Destroyer (q.v.) module that solves this if you load it first and then call "JE::Destroyer::destroy($j)" on the JE object when you have finished with it.)
* If you are using perl 5.9.3 or lower, then Tie::RefHash::Weak is required. Recent versions of it require Variable::Magic, an XS module (which requires a compiler of course), but version 0.02 of the former is just pure Perl with no XS dependencies.
There is currently an experimental version of the run-time engine, which is supposed to be faster, although it currently makes compilation slower. (If you serialise the compiled code and use that, you should notice a speed-up.) It will eventually replace the current one when it is complete. (It does not yet respect tainting or max_ops, or report line numbers correctly.) You can activate it by setting to 1 the ridiculously named YES_I_WANT_JE_TO_OPTIMISE environment variable, which is just a temporary hack that will later be removed.
If you simply need to run a few JS functions from Perl, create a new JS environment like this:
my $je = new JE;
If necessary, make Perl subroutines available to JavaScript:
$je->new_function(warn => sub { warn @_ }); $je->new_function(ok => \&Test::More::ok);
Then pass the JavaScript functions to "eval":
$je->eval(<<'___'); function foo() { return 42 } // etc. ___ # or perhaps: use File::Slurp; $je->eval(scalar read_file 'functions.js');
Then you can access those function from Perl like this:
$return_val = $je->{foo}->(); $return_val = $je->eval('foo()');
The return value will be a special object that, when converted to a string, boolean or number, will behave exactly as in JavaScript. You can also use it as a hash, to access or modify its properties. (Array objects can be used as arrays, too.) To call one of its JS methods, you should use the "method" method: "$return_val->method('foo')". See JE::Types for more information.
To create a custom global object, you have to subclass JE. For instance, if all you need to do is add a "self" property that refers to the global object, then override the "new" method like this:
package JEx::WithSelf; @ISA = 'JE'; sub new { my $self = shift->SUPER::new(@_); $self->{self} = $self; return $self; }
See "bind_class", below.
See JE::Types.
See also "JE::Object", which this class inherits from, and "JE::Types".
The (optional) options it can take are "max_ops" and "html_mode", which correspond to the methods listed below.
If the syntax is not valid, "undef" will be returned and $@ will contain an error message. Otherwise $@ will be a null string.
The JE::Code class provides the method "execute" for executing the pre-compiled syntax tree.
$filename and $first_line_no, which are both optional, will be stored inside the JE::Code object and used for JS error messages. (See also add_line_number in the JE::Code man page.)
$j->eval('[1,2,3]') # returns a JE::Object::Array which can be used as # an array ref
If $filename and $lineno are specified, they will be used in error messages. $lineno is the number of the first line; it defaults to 1.
If an error occurs, "undef" will be returned and $@ will contain the error message. If no error occurs, $@ will be a null string.
This is actually just a wrapper around "parse" and the "execute" method of the JE::Code class.
If the JavaScript code evaluates to an lvalue, a JE::LValue object will be returned. You can use this like any other return value (e.g., as an array ref if it points to a JS array). In addition, you can use the "set" and "get" methods to set/get the value of the property to which the lvalue refers. (See also JE::LValue.) E.g., this will create a new object named "document":
$j->eval('this.document')->set({});
Note that I used "this.document" rather than just "document", since the latter would throw an error if the variable did not exist.
Use this to make a Perl subroutine accessible from JavaScript.
For more ways to create functions, see JE::Object::Function.
This is actually a method of JE::Object, so you can use it on any object:
$j->{Math}->new_function(double => sub { 2 * shift });
$j->eval('String.prototype')->new_method( reverse => sub { scalar reverse shift } ); # ... then later ... $j->eval(q[ 'a string'.reverse() ]); # returns 'gnirts a'
With no arguments, this method returns the current value.
As shorthand, you can pass "max_ops => $foo" to the constructor.
If the number of operations is exceeded, then "eval" will return undef and set $@ to a 'max_ops (xxx) exceeded.
With no arguments, this method returns the current value.
As shorthand, you can pass "html_mode => 1" to the constructor.
If you pass it more than one argument in scalar context, it returns the number of arguments--but that is subject to change, so don't do that.
$j->bind_class( package => 'Net::FTP', name => 'FTP', # if different from package constructor => 'new', # or sub { Net::FTP->new(@_) } methods => [ 'login','get','put' ], # OR: methods => { log_me_in => 'login', # or sub { shift->login(@_) } chicken_out => 'quit', } static_methods => { # etc. etc. etc. } to_primitive => \&to_primitive # or a method name to_number => \&to_number to_string => \&to_string props => [ 'status' ], # OR: props => { status => { fetch => sub { 'this var never changes' } store => sub { system 'say -vHysterical hah hah' } }, # OR: status => \&fetch_store # or method name }, static_props => { ... } hash => 1, # Perl obj can be used as a hash array => 1, # or as an array # OR (not yet implemented): hash => 'namedItem', # method name or code ref array => 'item', # likewise # OR (not yet implemented): hash => { fetch => 'namedItem', store => sub { shift->{+shift} = shift }, }, array => { fetch => 'item', store => sub { shift->[shift] = shift }, }, isa => 'Object', # OR: isa => $j->{Object}{prototype}, ); # OR: $j->bind_class( package => 'Net::FTP', wrapper => sub { new JE_Proxy_for_Net_FTP @_ } );
(Some of this is random order, and probably needs to be rearranged.)
This method binds a Perl class to JavaScript. LIST is a hash-style list of key/value pairs. The keys, listed below, are all optional except for "package" or "name"--you must specify at least one of the two.
Whenever it says you can pass a method name to a particular option, and that method is expected to return a value (i.e., this does not apply to "props => { property_name => { store => 'method' } }"), you may append a colon and a data type (such as ':String') to the method name, to indicate to what JavaScript type to convert the return value. Actually, this is the name of a JS function to which the return value will be passed, so 'String' has to be capitalised. This also means than you can use 'method:eval' to evaluate the return value of 'method' as JavaScript code. One exception to this is that the special string ':null' indicates that Perl's "undef" should become JS's "null", but other values will be converted the default way. This is useful, for instance, if a method should return an object or "null", from JavaScript's point of view. This ':' feature does not stop you from using double colons in method names, so you can write 'Package::method:null' if you like, and rest assured that it will split on the last colon. Furthermore, just 'Package::method' will also work. It won't split it at all.
If it is a coderef, it will be used as the constructor.
If this is omitted, the constructor will raise an error when called. If there is already a constructor with the same name, however, it will be left as it is (though methods will still be added to its prototype object). This allows two Perl classes to be bound to a single JavaScript class:
$j->bind_class( name => 'Foo', package => 'Class::One', methods => ... ); $j->bind_class( name => 'Foo', package => 'Class::Two' );
If a hash ref is used, the keys will be the names of the methods from JavaScript's point of view. The values can be either the names of the Perl methods, or code references.
If to_primitive is omitted, the usual valueOf and toString methods will be tried as with built-in JS objects, if the object does not have overloaded string/boolean/number conversions. If the object has even one of those three, then conversion to a primitive will be the same as in Perl.
If "to_primitive => undef" is specified, primitivisation without a hint (which happens with "<" and "==") will throw a TypeError.
If this is an array ref, its elements will be the names of the properties. When a property is retrieved, a method of the same name is called. When a property is set, the same method is called, with the new value as the argument.
If a hash ref is given, for each element, if the value is a simple scalar, the property named by the key will trigger the method named by the value. If the value is a coderef, it will be called with the object as its argument when the variable is read, and with the object and the new value as its two arguments when the variable is set. If the value is a hash ref, the "fetch" and "store" keys will be expected to be either coderefs or method names. If only "fetch" is given, the property will be read-only. If only "store" is given, the property will be write-only and will appear undefined when accessed. (If neither is given, it will be a read-only undefined property--really useful.)
The value you give this option should be one of the strings '1-way' and '2-way' (also 1 and 2 for short).
If you specify '1-way', only properties corresponding to existing hash elements will be linked to those elements; properties added to the object from JavaScript will be JavaScript's own, and will not affect the wrapped object. (Consider how node lists and collections work in web browsers.)
If you specify '2-way', an attempt to create a property in JavaScript will be reflected in the underlying object.
To do: Make this accept '1-way:String', etc.
To do: Make this accept '1-way:String', etc.
This is experimental right now. I might actually make this the default. Maybe this should provide more options for fine-tuning, or maybe what is currently the default behaviour should be removed. If anyone has any opinions on this, please e-mail the author.
The name of the superclass. 'Object' is the default. To make this new class's prototype object have no prototype, specify "undef". Instead of specifying the name of the superclass, you can provide the superclass's prototype object.
If you specify a name, a constructor function by that name must already exist, or an exception will be thrown. (I supposed I could make JE smart enough to defer retrieving the prototype object until the superclass is registered. Well, maybe later.)
When an object of the Perl class in question is 'upgraded,' this subroutine will be called with the global object as its first argument and the object to be 'wrapped' as the second. The subroutine is expected to return an object compatible with the interface described in JE::Types.
If "wrapper" is supplied, no constructor will be created.
After a class has been bound, objects of the Perl class will, when passed to JavaScript (or the "upgrade" method), appear as instances of the corresponding JS class. Actually, they are 'wrapped up' in a proxy object (a JE::Object::Proxy object), that provides the interface that JS operators require (see JE::Types). If the object is passed back to Perl, it is the proxy, not the original object that is returned. The proxy's "value" method will return the original object. But, if the "unwrap" option above is used when a class is bound, the original Perl object will be passed to any methods or properties belonging to that class. This behaviour is still subject to change. See "unwrap", above.
Note that, if you pass a Perl object to JavaScript before binding its class, JavaScript's reference to it (if any) will remain as it is, and will not be wrapped up inside a proxy object.
To use Perl's overloading within JavaScript, well...er, you don't have to do anything. If the object has "", "0+" or "bool" overloading, that will automatically be detected and used.
If a piece of JS code is tainted, you can still run it, but any strings or numbers returned, assigned or passed as arguments by the tainted code will be tainted (even if it did not originated from within the code). E.g.,
use Taint::Util; taint($code = "String.length"); $foo = 0 + new JE ->eval($code); # $foo is now tainted
This does not apply to string or number objects, but, if the code created the object, then its internal value will be tainted, because it created the object by passing a simple string or number argument to a constructor.
Apart from items listed under "BUGS", below, JE follows the ECMAScript v3 specification. There are cases in which ECMAScript leaves the precise semantics to the discretion of the implementation. Here is the behaviour in such cases:
The spec. states that, whenever it (the spec.), say to throw a SyntaxError, an implementation may provide other behaviour instead. Here are some instances of this:
var thing = eval('return "foo"; this = statement(is,not) + executed')
JE also supports the "escape" and "unescape" global functions (not part of ECMAScript proper, but in the appendix).
To report bugs, please e-mail the author.
Fixing this is a bit complicated. If anyone would like to help, please let me know. (The problem is that the same code would be repeated a dozen times in "bind_class"'s closures--a maintenance nightmare likely to result in more security bugs. Is there any way to eliminate all those closures?)
foo( (4))
--, if "foo" is not a function, line 2 will be reported instead of line 1.
"".new JE ->eval(q| Function('foo','return[a]')() | )
with(o={x:1})var x = (delete x,5); return o.x
eval(' 3; try{}finally{5} ')
a = 1; a += (a=2,"b")
$j = new JE; $j->new_function(outta_here => sub { last outta }); outta: { $j->eval(' try { x = 1; outta_here() } finally { x = 2 } '); } print $j->{x}, "\n";
...that are probably due to typos in the spec.
I believe there is a typo in the spec. in clause 12.14, in the 'TryStatement : try Block Catch Finally' algorithm. Step 5 should probably read 'Let C = Result(4),' rather than 'If Result(4).type is not normal, Let C = Result(4).'
I think this is also a typo in the spec. In the first algorithm in clause 12.6.3, step 1 should probably read 'If ExpressionNoIn is not present, go to step 4,' rather than 'If the first Expression is not present, go to step 4.'
I think I've found yet another typo in the spec. In clause 15.9.5.27, 'Result(1)' and and 'Result(2)' are probably supposed to be 'Result(2)' and 'Result(3)', respectively.
perl 5.8.4 or higher
Scalar::Util 1.14 or higher
Exporter 5.57 or higher
Tie::RefHash::Weak, for perl versions earlier than 5.9.4
The TimeDate distribution (more precisely, Time::Zone and Date::Parse)
Encode 2.08 or higher
Note: JE will probably end up with Unicode::Collate in the list of dependencies.
Copyright (C) 2007-14 Father Chrysostomos <sprout [at] cpan [dot] org>
This program is free software; you may redistribute it and/or modify it under the same terms as perl.
Some of the code was derived from Data::Float, which is copyrighted (C) 2006, 2007, 2008 by Andrew Main (Zefram).
Some of the
Thanks to Max Maischein, Kevin Cameron, Chia-liang Kao and Damyan Ivanov for their contributions,
to Andy Armstrong, Yair Lenga, Alex Robinson, Christian Forster, Imre Rad, Craig Mackenna and Toby Inkster for their suggestions,
and to the CPAN Testers for their helpful reports.
The other JE man pages, especially the following (the rest are listed on the JE::Types page):
ECMAScript Language Specification (ECMA-262)
JavaScript.pm, JavaScript::SpiderMonkey and JavaScript::Lite--all interfaces to Mozilla's open-source SpiderMonkey JavaScript engine.
JavaScript::V8
WWW::Mechanize::Plugin::JavaScript
2021-01-08 | perl v5.32.0 |