Skip to content

Instantly share code, notes, and snippets.

@jgomer2001
Last active February 7, 2022 21:59
Show Gist options
  • Save jgomer2001/7acbe479bdffba6bcce8bfbb7122e314 to your computer and use it in GitHub Desktop.
Save jgomer2001/7acbe479bdffba6bcce8bfbb7122e314 to your computer and use it in GitHub Desktop.
serialization/deserialization of Rhino continuations
//package ...;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamClass;
import java.io.OutputStream;
import java.util.stream.Stream;
import org.mozilla.javascript.NativeContinuation;
import org.mozilla.javascript.NativeJavaObject;
import org.mozilla.javascript.Scriptable;
public class ContinuationSerializer {
private CustClassLoader classLoader;
public ContinuationSerializer(CustClassLoader classLoader) {
this.classLoader = classLoader;
}
public byte[] save(Scriptable scope, NativeContinuation continuation) throws IOException {
class CustomObjectOutputStream extends ObjectOutputStream {
CustomObjectOutputStream(OutputStream out) throws IOException {
super(out);
enableReplaceObject(true);
}
@Override
protected Object replaceObject(Object obj) throws IOException {
//should be NativeJavaObject.class.isInstance(obj) ?
if (obj != null && obj.getClass().equals(NativeJavaObject.class)) {
NativeJavaObject njo = (NativeJavaObject) obj;
//The "custom" classloader allows me to know the classes it has loaded its own (not via parent delegation)
//If your classloader is not that "smart", you can still make an instance of NativaJavaObjectBox (all incoming Java objects will be boxed)
if (Stream.of(classLoader.getLoadedClasses()).anyMatch(njo.unwrap().getClass()::equals)) {
return new NativeJavaObjectBox(njo);
}
}
return super.replaceObject(obj);
}
}
try (
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream sos = new CustomObjectOutputStream(baos)) {
sos.writeObject(new Object[] { scope, continuation });
return baos.toByteArray();
}
}
public Pair<Scriptable, NativeContinuation> restore(byte[] data) throws IOException {
class CustomObjectInputStream extends ObjectInputStream {
public CustomObjectInputStream(InputStream in) throws IOException {
super(in);
enableResolveObject(true);
}
@Override
public Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
return Class.forName(desc.getName(), false, classLoader);
}
@Override
protected Object resolveObject(Object obj) throws IOException {
//should be NativeJavaObjectBox.class.isInstance(obj) ?
if (obj != null && obj.getClass().equals(NativeJavaObjectBox.class)) {
return ((NativeJavaObjectBox) obj).getRaw();
}
return super.resolveObject(obj);
}
}
try (
ByteArrayInputStream bais = new ByteArrayInputStream(data);
ObjectInputStream sis = new CustomObjectInputStream(bais)) {
Object[] arr = (Object[]) sis.readObject();
return new Pair<>((Scriptable) arr[0], (NativeContinuation) arr[1]);
} catch (ClassNotFoundException e) {
throw new IOException(e);
}
}
}
//package ...;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import org.mozilla.javascript.NativeJavaObject;
import org.mozilla.javascript.Scriptable;
import org.slf4j.Logger;
public class NativeJavaObjectBox implements Serializable {
private static final long serialVersionUID = ...;
private static final Logger logger = ...;
private final Object unwrapped;
private NativeJavaObject raw;
public NativeJavaObjectBox(NativeJavaObject raw) {
this.raw = raw;
unwrapped = raw.unwrap();
logger.trace("NativeJavaObjectBox created with underlying {}", unwrapped.getClass().getName());
}
private void writeObject(ObjectOutputStream out) throws IOException {
logger.trace("Writing NativeJavaObjectBox");
out.writeObject(raw.getParentScope());
out.writeObject(unwrapped);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
logger.trace("Reading NativeJavaObjectBox");
Scriptable parentScope = (Scriptable) in.readObject();
Object object = in.readObject();
logger.trace("Underlying object is an instance of {}", object.getClass().getName());
raw = new NativeJavaObject(parentScope, object, object.getClass());
}
public NativeJavaObject getRaw() {
logger.debug("Returning raw instance");
return raw;
}
}

This is a working example on how to make serialization/deserialization of Rhino continuations when some classes originate from a classloader other than the main application classloader, see mozilla/rhino#1178

The solution depends heavily on how NativeJavaObject class is defined/structured, so it may not work in future versions of Rhino (tested in 1.7.14 only). It looks like a hack but IMO there is no other way to make this use case feasible with the library as is. This is due to how NativeJavaObjects are deserialized: it makes use of the "current" classloader only.

To try it, instantiate ContinuationSerializer (you may need to adjust the constructor signature) and supply your classloader. Then call save (serialize) and restore (deserialize)

It is assumed the parent of such classloader is the classloader that most of application classes belong to. It is also assumed the usual delegation model is followed for loading classes (ie. asking for parents first). Only serializable classes expected.

Note: There is no support for Java adpaters.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment