Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Clone Entire JavaScript ScriptEngine

I need to somehow deep clone the entire set of bindings of my ScriptEngine object.

What I have tried

  • I have tried so far the Cloner library to clone the entire Bindings structure. This would be great if it worked because it would have ensured a precise copy, including private variables. But this leads to jvm heap corruption (the jvm just crashes with exit code -1073740940). Sometimes it doesn't crash but weird things happen, like the System.out.println() stops working as it should...

  • I have also looked into cloning the objects using js code inside the ScriptEngine, so that I can get those as NativeObjects and manage them in some java maps. But all cloning methods which I found have flaws. I want a precise snapshot of the objects. For instance if each of two objects a and b contain fields (say a.fa and b.fb) which reference the same object c, when cloned using jQuery.extend() (for instance) the fields a.fa and b.fb of the cloned a and b will reference different clones of c, instead of referencing one same clone. And many other edge issues.

  • I also tried to clone the entire ScriptEngine using Cloner (not only the bindings), and I also tried using Rhino's js engine and clone the entire scope (instead of the bundeled ScriptEngine wrapper). But the heap corruption issue persists.

Why I need to do it

I need this because I must be able to restore the values of the entire ScriptEngine bindings to some previous point. I need to make precise snapshots of the bindings.

The application is part of my doctoral research project which consists of running state machines with nodes (implemented in java) which have js code attached. The js code is typed in by the end user and it is being evaled at runtime. When final state can't be reached through a path, the algorithm makes steps backwards, trying to find alternative paths. On each step backward it must undo any changes that might have occurred in the js engine bindings.


All the global variables names are known before js eval-ing, and are objects (the user types in code for the nodes and this is then organized (within java) into js objects with certain name patterns). But their content can be anything because that is controlled by the user js code.

So I guess my only sollution now is to clone js object using js code.

like image 597
Radu Simionescu Avatar asked Jun 20 '12 09:06

Radu Simionescu


2 Answers

Aside from "edge cases", jQuery.extend can be used in the way you mention. a b and their clones will all reference the same object c.

var c = { f:'see' };
var a = { fa: c };
var b = { fb: c };
var cloneA = $.extend({}, a);
var cloneB = $.extend({}, b);
console.log(a.fa === b.fb, cloneA.fa === cloneB.fb, a.fa === cloneB.fb);
// true true true

But it seems like you want to clone all the objects (including c) while keeping track of the objects' relationships. For this, it may be best to use object relation tables.

I see this a lot with nested javascript objects and JSON because people tend to forget that JSON is purely a text format. There are no actual javascript objects in a JSON file except for a single string of text instanceof String. There are no beans or pickles or any preservative-heavy foods in javascript.

In an object relation table, each 'table' is just an array of "flat" objects with only primitive valued properties and pointers (not references) to other objects in the table (or in another table). The pointer can just be the index of the target object.

So a JSON version of the above object relationship might look something like

{
    "table-1":[
        { "a": { "fa":["table-2",0] } },
        { "b": { "fb":["table-2",0] } }
    ],
    "table-2":[
        { "c": { "name":"see" } },
        { "d": { "name":"dee" } },
        { "e": { "name":"eh.."} }
    ]
}

And a parser might look like

var tables = JSON.parse(jsonString);
for(var key in tables){
    var table = tables[key];
    for(var i = 0; i < table.length; i++){
        var name = Object.keys(table[i])
        var obj = table[i][name];
        for(var key2 in obj){
            if(obj[key2] instanceof Array && obj[key2][0] in tables){
                var refTable = obj[key2][0];
                var refID    = obj[key2][1];
                var refObj   = tables[refTable][refID];
                var refKey   = Object.keys(refObj)[0];
                obj[key2] = refObj[refKey];
            }
        }
        this[name] = obj;
    }
}

console.log(a.fa === b.fb, b.fb === c);
// true true

I realize object relationship mapping has it's downfalls, but taking snapshots of the scripting engine does sound a bit crazy. Especially since your intention is to be able to recall each previous step, because then you need a new snapshot for every step... that will very quickly take up shit ton of disk space.. unless you're just keeping track of the snapshot diffs between each step, like a git repo. That sounds like an awful lot of work to implement a seemingly simple "undo" method.

Damn.. come to think of it, why not just store each step in a history file? Then if you need to step back, just truncate the history file at the previous step, and run each step over again in a fresh environment.

Not sure how practical that would be (performance wise) using java. Nodejs (as it exists today) is faster than any java scripting engine will ever be. In fact, I'm just gonna call it ECMAscript from now on. Sorry bout the rant, its just that

Java is slow, but you already know.
Because it readily shows, that's as fast as it goes.

like image 120
Julian Avatar answered Sep 21 '22 14:09

Julian


May I suggest a different approach.

  • Don't try to clone the ScriptEngine. It doesn't implement Serializable/Externalizable and the API doesn't support cloning. Attempts to force a clone will be workarounds that might break in some future Java version.

  • Serialize Bindings to JSON using cycle.js. It will encode object references in the form {$ref: PATH}. When you deserialize it will restore the references.

    As far as I can tell cycle.js doesn't serialize functions but its possible to add function serialization yourself using Function.toString() (See below and Example)

Alternatively if using a library is not an option its fairly straightforward to implement your own serialization to suit your needs:

var jsonString = JSON.stringify(obj, function(key, val) {
    if (typeof(value) === 'function')
        return val.toString();
    // or do something else with value like detect a reference
    return val
})
like image 38
Andrejs Avatar answered Sep 22 '22 14:09

Andrejs