Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a JRuby extension for the project, including build configurations, language definitions, and a method wrapper for Java interoperability. Key feedback includes addressing thread-safety concerns in the JRubyExtension language definition caching, preventing a potential NullPointerException when handling stack traces with missing file names, and cleaning up redundant logic and inconsistent thread stack management in the FWrapper class.
| if (languageDefinition == null) { | ||
| ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); | ||
| Thread.currentThread().setContextClassLoader(JRubyExtension.class.getClassLoader()); | ||
| languageDefinition = new JRubyLanguageDefinition(this, runner); | ||
| Thread.currentThread().setContextClassLoader(classLoader); | ||
| } |
There was a problem hiding this comment.
The initialization of the static languageDefinition field is not thread-safe, which could lead to multiple instances being created if getLanguage is called concurrently. Furthermore, since JRubyLanguageDefinition is initialized with a specific Core instance, using a static field to cache it is problematic if multiple Core instances exist (as suggested by the PR description). The cached instance would remain tied to the first runner that initialized it. Consider using a ConcurrentHashMap to store language definitions per runner.
if (languageDefinition == null) {
synchronized (JRubyExtension.class) {
if (languageDefinition == null) {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(JRubyExtension.class.getClassLoader());
languageDefinition = new JRubyLanguageDefinition(this, runner);
} finally {
Thread.currentThread().setContextClassLoader(classLoader);
}
}
}
}| if (current.getClassName().startsWith("org.jruby")) return elements.hasNext() ? traceStack(elements.next(), elements) : null; | ||
| BaseWrappedException.SourceLocation loc; | ||
| if (current.getClassName().equals("RUBY")) { | ||
| loc = new BaseWrappedException.GuestLocation(new File(current.getFileName()), -1, -1, current.getLineNumber(), -1); |
There was a problem hiding this comment.
StackTraceElement.getFileName() can return null if the source information is unavailable. Passing null to the File constructor will result in a NullPointerException.
| loc = new BaseWrappedException.GuestLocation(new File(current.getFileName()), -1, -1, current.getLineNumber(), -1); | |
| String fileName = current.getFileName(); | |
| loc = new BaseWrappedException.GuestLocation(fileName != null ? new File(fileName) : new File("unknown"), -1, -1, current.getLineNumber(), -1); |
| Semaphore lock = new Semaphore(0); | ||
| boolean joinedThread = ctx.runner.profile.checkJoinedThreadStack(); | ||
|
|
||
| Thread t = new Thread(() -> { | ||
| ctx.bindThread(Thread.currentThread()); | ||
|
|
||
| try { | ||
| ThreadContext threadContext = ctx.getContext().getProvider().getRuntime().getCurrentContext(); | ||
| threadContext.pushNewScope(threadContext.getCurrentStaticScope()); | ||
| IRubyObject[] rubyObjects = JavaUtil.convertJavaArrayToRuby(threadContext.runtime, params); | ||
| fn.call(threadContext, rubyObjects, threadContext.getFrameBlock()).toJava(Object.class); | ||
| } catch (Throwable ex) { | ||
| ctx.runner.profile.logError(ex); | ||
| } finally { | ||
| ctx.unbindThread(Thread.currentThread()); | ||
| ctx.runner.profile.joinedThreadStack.remove(Thread.currentThread()); | ||
|
|
||
| ctx.releaseBoundEventIfPresent(Thread.currentThread()); | ||
|
|
||
| lock.release(); | ||
| } |
There was a problem hiding this comment.
The Semaphore lock is redundant as it is never acquired in the calling thread. Additionally, the joinedThread variable is fetched but never used to add the thread to the joinedThreadStack (unlike in inner_apply), which makes the cleanup at line 71 potentially inconsistent. The logic should ensure the thread is added to the stack if checkJoinedThreadStack() returns true.
boolean joinedThread = ctx.runner.profile.checkJoinedThreadStack();
Thread t = new Thread(() -> {
ctx.bindThread(Thread.currentThread());
if (joinedThread) {
ctx.runner.profile.joinedThreadStack.add(Thread.currentThread());
}
try {
ThreadContext threadContext = ctx.getContext().getProvider().getRuntime().getCurrentContext();
threadContext.pushNewScope(threadContext.getCurrentStaticScope());
IRubyObject[] rubyObjects = JavaUtil.convertJavaArrayToRuby(threadContext.runtime, params);
fn.call(threadContext, rubyObjects, threadContext.getFrameBlock()).toJava(Object.class);
} catch (Throwable ex) {
ctx.runner.profile.logError(ex);
} finally {
ctx.unbindThread(Thread.currentThread());
ctx.runner.profile.joinedThreadStack.remove(Thread.currentThread());
ctx.releaseBoundEventIfPresent(Thread.currentThread());
}|
Thanks @gemini-code-assist — responding inline to each of the three review points. All three land on code that exists in upstream 1. Static This one is a deliberate divergence from upstream, not a copy: I matched 2. Genuine pre-existing bug — 3. Redundant Also a pre-existing upstream bug — To be explicit: the PR description's "Changes vs upstream" list enumerates every semantic adaptation I made. Any behavior change not on that list — including fixes to latent bugs — is intentionally scoped out of this PR. Reviewers who want any of these fixes folded in before merge, let me know and I'll push an extra commit. |
|
Thanks for the detailed breakdown, @grepsedawk. I appreciate the clarification on the design choices and the commitment to a faithful port. Given that these issues are pre-existing in the upstream |
9d636e4 to
8b73418
Compare
8b73418 to
7b2f463
Compare
Adds a JRuby 9.4.5 language extension as :extension:ruby, providing .rb script support via a new ExtensionSpec(extId = "jruby") entry in stonecutter.gradle.kts. Ported from grepsedawk/JsMacros-Ruby (itself a fork of the original wagyourtail jsmacros-ruby). JRuby is embedded into the output jar under META-INF/jsmacroscedeps/. Adapted for CE's split LanguageExtension/LibraryExtension API and the Core<?, ?> runner-scoped context constructors (upstream predates both). Preload and JavaWrapper async threads are daemon + named. Fixes applied atop the straight port: - Daemon flags on spawned threads (upstream leaked non-daemon threads). - Removed a dead Semaphore + unused local in FWrapper.inner_accept. - Extracted a shared callFn helper to deduplicate the three near-identical JRuby call sites in FWrapper. Script bindings use local variables context / event / file (no dollar prefix), matching the pre-139c8ce wagyourtail convention. Legacy scripts that reference bare context break under the post-139c8ce global-only form; this restores compatibility. Vestigial bits dropped from upstream: JRubyConfig (option was never registered) and its single translation key. Build-verified against stonecutter versions 1.21.5, 1.21.8, 1.21.10, 1.21.11. Extension is MC-API-free, so the same classes build unchanged across versions. Tested via an in-process ServiceLoader smoke test (jshell, JDK 21): confirms discovery of JRubyExtension, resolution of the embedded jruby-complete jar, and that org.jruby.embed.ScriptingContainer loads from it. Not smoke-tested in the MC run environment.
a52fcf8 to
d823ffa
Compare
|
Re-opened against upstream at JsMacrosCE#21. Unstacking from feat/mc-26.1 — the Ruby extension has no direct MC-API dependencies and doesn't need to wait on the 26.1 port. |
Stacked on #1. Review after #1 lands; base is
feat/mc-26.1until then. Includes 3 upstream-bug fixes (daemon threads, deadSemaphore, dedup helper) — see commit8b73418.Summary
Port the JRuby scripting extension from grepsedawk/jsmacros-ruby into CE as
extension:ruby, following theextension:graallayout. Registers a new stonecutterExtensionSpec(path=":extension:ruby", extId="jruby")so it gets picked up by the multi-version build.Faithful 1:1 port of the upstream source with the minimum edits needed to compile against CE's split
LanguageExtension/LibraryExtensionAPI and CE'sCore<?,?> runner-scoped language context constructors. JRuby 9.4.5.0 is embedded into the output jar underMETA-INF/jsmacroscedeps/.What's in the diff
extension/ruby/build.gradle.kts— embedsjruby-complete:9.4.5.0, wires processResources placeholder for the dep manifest.extension/ruby/src/main/java/com/jsmacrosce/jsmacros/jruby/client/JRubyExtension.java—LanguageExtension + LibraryExtensionimpl.extension/ruby/src/main/java/com/jsmacrosce/jsmacros/jruby/language/impl/JRubyLanguageDefinition.java—BaseLanguage<ScriptingContainer, JRubyScriptContext>impl.extension/ruby/src/main/java/com/jsmacrosce/jsmacros/jruby/language/impl/JRubyScriptContext.java—BaseScriptContext<ScriptingContainer>impl.extension/ruby/src/main/java/com/jsmacrosce/jsmacros/jruby/library/impl/FWrapper.java—JavaWrapperlibrary for Ruby scripts; preserves the upstream async-invocation fix.extension/ruby/src/main/resources/jsmacrosce.ext.jruby.json+META-INF/services/...Extension— CE extension manifest + SPI registration.settings.gradle.kts+stonecutter.gradle.kts— register the new subproject.Changes vs upstream jsmacros-ruby
xyz.wagyourtail.jsmacros.*→com.jsmacrosce.jsmacros.*.Extension→LanguageExtension + LibraryExtension(CE API change). RenamedgetLanguageImplName()→getExtensionName().init(Core<?,?> runner)now takes a runner (CE dropped theCore.getInstance()singleton).FWrapperrewritten to usectx.runner.profilethroughout.JRubyScriptContextconstructor signature changed to match CE'sBaseScriptContext(Core<?,?>, BaseEvent, File). Null-guard added incloseContext()to match CE conventions.FWrappergenerics tightened: constructor param is nowClass<? extends BaseLanguage<ScriptingContainer, JRubyScriptContext>>,inner_applyreturn is<R2>rather thanObject.META-INF/jsmacrosdeps/→META-INF/jsmacroscedeps/to match CE'sExtension.getDependenciesInternallookup.init()now callsinstance.terminate()(upstream leaked it), and the preload / asyncJavaWrapperthreads are daemon + named.Semaphore+ unused local inFWrapper.inner_accept; extracted sharedcallFnhelper to deduplicate the three near-identical JRuby-call sites.compileOnly("org.jetbrains:annotations:20.1.0")— upstream usedjavax.annotation.Nullable; CE convention is jetbrains.Intentionally dropped from upstream
JRubyConfig/useGlobalContextoption — was never registered upstream (theaddOptionscall was commented out) andJRubyLanguageDefinitionnever reads the option. Deleting the vestigial class keeps the PR smaller. Follow-up can land it if the option is wanted.assets/jsmacros/jruby/lang/en_us.json— the only translation key was for the unregistered config option. Can come back with the config in a follow-up.assets/jsmacros-ruby/icon.png— upstream mod icon, not applicable to an extension subproject.Build verification
Built
:extension:ruby:jarafter switchingstonecutter.activeto each of:1.21.5✓1.21.8✓1.21.10✓1.21.11✓Output jar
extension/ruby/build/libs/jsmacrosce-ruby-2.0.0.jar(~29.8 MB) on 1.21.8 contains:JRubyExtension,JRubyLanguageDefinition+$Executor,JRubyScriptContext,FWrapper+$RubyMethodWrapper)jsmacrosce.ext.jruby.jsonwith thedependenciesplaceholder correctly expanded to["META-INF/jsmacroscedeps/jruby-complete-9.4.5.0.jar"]META-INF/services/com.jsmacrosce.jsmacros.core.extensions.Extension→JRubyExtensionMETA-INF/jsmacroscedeps/jruby-complete-9.4.5.0.jarembedded (33.5 MB)Extension has no direct MC-API dependencies, so the same compiled classes are reused across stonecutter versions (compileJava task was UP-TO-DATE for 1.21.5/10/11 after 1.21.8 built).
Runtime verification
ServiceLoader + embedded-jar smoke test passed via jshell on JDK 21 (MC 1.21.8 stonecutter-active):
ServiceLoader.load(Extension.class, <cp with ruby jar + common + gson>)discoversJRubyExtension✓getExtensionName()→"jruby",minCoreVersion=2.0.0,maxCoreVersion=2.1.0✓getDependencies()returns the embeddedMETA-INF/jsmacroscedeps/jruby-complete-9.4.5.0.jarURL ✓LanguageExtensionandLibraryExtension✓org.jruby.embed.ScriptingContainerresolves from it ✓Test plan
.rbscript via the mod UI and verify it executes.JavaWrapper.methodToJavacan register a Ruby callback that the host invokes.Not in scope for this PR
feat/mc-26.1(separate teammate). Ruby will inherit via stonecutter once that lands.JRubyConfig.useGlobalContextas a real option.