Skip to content

Instantly share code, notes, and snippets.

@dmikurube
Last active February 4, 2021 07:03
Show Gist options
  • Save dmikurube/f8df821d4527c8f1ab18efceba159173 to your computer and use it in GitHub Desktop.
Save dmikurube/f8df821d4527c8f1ab18efceba159173 to your computer and use it in GitHub Desktop.
Load JRuby via a sub ClassLoader
*~
/.gradle/
/build/
apply plugin: "java"
repositories {
mavenCentral()
}
configurations {
jruby
}
dependencies {
compile "joda-time:joda-time:2.9.2"
compile "com.google.inject:guice:4.0"
compile "com.google.inject.extensions:guice-multibindings:4.0"
jruby "org.jruby:jruby-complete:9.1.15.0"
}
sourceSets {
main {
java {
srcDir "."
}
}
}
task installGems1(type: JavaExec) {
doFirst {
delete("${buildDir}/dependencyGems1")
mkdir("${buildDir}/dependencyGems1")
}
classpath = configurations.jruby
main = "org.jruby.Main"
args = ["-rjars/setup", "-S", "gem", "install", "msgpack:1.1.0"]
environment "GEM_HOME": "${buildDir}/dependencyGems1"
}
task installGems2(type: JavaExec) {
doFirst {
delete("${buildDir}/dependencyGems2")
mkdir("${buildDir}/dependencyGems2")
}
classpath = configurations.jruby
main = "org.jruby.Main"
args = ["-rjars/setup", "-S", "gem", "install", "sigdump:0.2.4"]
environment "GEM_HOME": "${buildDir}/dependencyGems2"
}
task run(type: JavaExec, dependsOn: ["installGems1", "installGems2"]) {
main = "Main"
classpath = sourceSets.main.runtimeClasspath
systemProperty "jruby", configurations.jruby.files.collect { file -> file.toURI() }.join(";")
systemProperty "gemHome", "${buildDir}/dependencyGems1"
systemProperty "gemPath", "${buildDir}/dependencyGems1:${buildDir}/dependencyGems2"
}
task run100(dependsOn: ["installGems1", "installGems2"]) {
doFirst {
for (int i = 0; i < 100; i++) {
javaexec {
main = "Main"
classpath = sourceSets.main.runtimeClasspath
systemProperty "jruby", configurations.jruby.files.collect { file -> file.toURI() }.join(";")
systemProperty "gemHome", "${buildDir}/dependencyGems1"
systemProperty "gemPath", "${buildDir}/dependencyGems1:${buildDir}/dependencyGems2"
}
}
}
}
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.io.File;
class JRuby {
JRuby(final JRubyClassLoader classLoader) throws Exception {
this.classLoader = classLoader;
this.scriptingContainer = createScriptingContainer();
this.method_runScriptlet_LString = scriptingContainer.getClass().getMethod("runScriptlet", String.class);
this.method_callMethod_ALObject = scriptingContainer.getClass().getMethod(
"callMethod", Object.class, String.class, Object[].class);
}
Object runScriptlet(final String script) throws Exception {
return this.method_runScriptlet_LString.invoke(this.scriptingContainer, script);
}
void setGemPaths(final String gemHome, final String gemPath) throws Exception {
final String[] gemArgs;
// An empty string in GEM_PATH is still effective as GEM_PATH.
// https://svn.ruby-lang.org/cgi-bin/viewvc.cgi/tags/v2_6_2/lib/rubygems/path_support.rb?revision=67232&view=markup#l34
// https://svn.ruby-lang.org/cgi-bin/viewvc.cgi/tags/v2_6_2/lib/rubygems/path_support.rb?revision=67232&view=markup#l51
if (gemPath != null) {
// It is believed that Ruby's File::PATH_SEPARATOR is the same with Java's File.pathSeparator.
// https://github.com/jruby/jruby/blob/9.2.6.0/core/src/main/java/org/jruby/RubyFile.java#L122-L125
final String[] gemPaths = gemPath.split("\\" + File.pathSeparator);
gemArgs = new String[gemPaths.length + 1];
gemArgs[0] = gemHome;
for (int i = 0; i < gemPaths.length; ++i) {
gemArgs[i + 1] = gemPaths[i];
}
} else {
gemArgs = new String[2];
if (gemHome != null) {
gemArgs[0] = gemHome;
gemArgs[1] = gemHome;
} else {
gemArgs[0] = null;
gemArgs[1] = null;
}
}
this.callMethodArray(this.runScriptlet("Gem"), "use_paths", gemArgs);
}
private Object createScriptingContainer() throws Exception {
final Object object_LocalContextScope = this.createLocalContextScopeSINGLETHREAD();
final Object object_LocalVariableBehavior = this.createLocalVariableBehaviorPERSISTENT();
final Class<?> clazz = classLoader.loadClass("org.jruby.embed.ScriptingContainer");
final Constructor<?> constructor =
clazz.getConstructor(object_LocalContextScope.getClass(), object_LocalVariableBehavior.getClass());
return constructor.newInstance(object_LocalContextScope, object_LocalVariableBehavior);
}
private Object createLocalContextScopeSINGLETHREAD() throws Exception {
final Class<?> clazz = this.classLoader.loadClass("org.jruby.embed.LocalContextScope");
final Method valueOf = clazz.getMethod("valueOf", String.class);
return valueOf.invoke(null, "SINGLETHREAD");
}
private Object createLocalVariableBehaviorPERSISTENT() throws Exception {
final Class<?> clazz = classLoader.loadClass("org.jruby.embed.LocalVariableBehavior");
final Method valueOf = clazz.getMethod("valueOf", String.class);
return valueOf.invoke(null, "PERSISTENT");
}
public Object callMethodArray(final Object receiver, final String methodName, final Object[] args) throws Exception {
return this.method_callMethod_ALObject.invoke(this.scriptingContainer, receiver, methodName, args);
}
private final JRubyClassLoader classLoader;
private final Object scriptingContainer;
private final Method method_runScriptlet_LString;
private final Method method_callMethod_ALObject;
}
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Vector;
final class JRubyClassLoader extends URLClassLoader {
JRubyClassLoader(final Collection<URL> jarUrls, final ClassLoader parent) {
// The delegation parent ClassLoader is processed by the super class URLClassLoader.
super(jarUrls.toArray(new URL[0]), parent);
}
@Override
protected void addURL(final URL url) {
throw new UnsupportedOperationException("JRubyClassLoader does not support addURL.");
}
@Override
public void close() throws IOException {
super.close();
}
@Override
public URL[] getURLs() {
return super.getURLs();
}
/**
* Loads the class with the specified binary name from JRuby.
*
* <p>It prioritizes a class found by this class loader over a class loaded by the parent class loader.
*/
@Override
protected Class<?> loadClass(final String name, final boolean resolve) throws ClassNotFoundException {
synchronized (this.getClassLoadingLock(name)) {
// If a class of the specified name has already been loaded by this class loader, or the parent class loader,
// find the loaded class, and return it.
final Class<?> loadedClass = this.findLoadedClass(name);
if (loadedClass != null) {
return this.resolveClassIfNeeded(loadedClass, resolve);
}
// JRuby should use Joda-Time of embulk-core (on the top-level class loader), not of jruby-complete.
// Otherwise, embulk-core uses its own, and JRuby uses its own, then they wouldn't match.
//
// TODO: Remove the condition when embulk-core removes Joda-Time from its dependencies.
if (!name.startsWith("org.joda.time.")) {
// If a class of the specified name has not been loaded yet, and is found by this (not parent) class loader,
// find it, and return it.
try {
return this.resolveClassIfNeeded(this.findClass(name), resolve);
} catch (final ClassNotFoundException ignored) {
// Passing through intentionally.
}
}
// If a class of the specified name is found by this class loader (not by the parent class loader),
// find it, and return it.
try {
return this.resolveClassIfNeeded(this.getParent().loadClass(name), resolve);
} catch (final ClassNotFoundException ignored) {
// Passing through intentionally.
}
throw new ClassNotFoundException(name);
}
}
/**
* Finds the resource with the given name from JRuby.
*
* <p>It prioritizes a resource found by this class loader over a class loaded by the parent class loader.
*/
@Override
public URL getResource(final String name) {
final URL parentUrl = this.getParent().getResource(name);
if (parentUrl != null) {
return parentUrl;
}
return this.findResource(name);
}
@Override
public Enumeration<URL> getResources(final String name) throws IOException {
final Vector<URL> resources = new Vector<>();
final Enumeration<URL> parentResources = this.getParent().getResources(name);
while (parentResources.hasMoreElements()) {
resources.add(parentResources.nextElement());
}
final Enumeration<URL> childResources = this.findResources(name);
while (childResources.hasMoreElements()) {
resources.add(childResources.nextElement());
}
return resources.elements();
}
private Class<?> resolveClassIfNeeded(final Class<?> clazz, final boolean resolve) {
if (resolve) {
this.resolveClass(clazz);
}
return clazz;
}
}
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
public class Main {
public static void main(final String[] args) throws Exception {
final String jrubyPath = System.getProperty("jruby");
System.out.println(jrubyPath);
final String gemHome = System.getProperty("gemHome");
System.out.println(gemHome);
final String gemPath = System.getProperty("gemPath");
System.out.println(gemPath);
final ArrayList<URL> jrubyUrls = new ArrayList<>();
for (final String url : jrubyPath.split(";")) {
jrubyUrls.add(new URL(url));
}
final JRubyClassLoader classloader = new JRubyClassLoader(jrubyUrls, Main.class.getClassLoader());
final JRuby jruby = new JRuby(classloader);
jruby.setGemPaths(gemHome, gemPath);
jruby.runScriptlet("require 'sigdump'");
jruby.runScriptlet("require 'msgpack'");
jruby.runScriptlet("puts 'foo'");
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment