Skip to content

Commit 306ee51

Browse files
new internal VariableAccessor API for access to Ruby variables from Java
This introduces some minor behavior changes: - The "java type" of variables set via the RedBridge API is no longer stored. Thus a variable that has been set to `String[]` will now come back as a `RubyArray` object which can be used as a `List<String>`. The undocumented previous behavior was to convert the value back to the same type it was originally. - BiVariableMap no longer holds a copy of all variables; it only actually stores local varibles. All other types of variable are simply retrieved from either the runtime's global state, or from the given receiver object, if and when the variable is accessed. - As a corollary, variables can now change value between the evaluation and the moment when they are accessed via BiVariableMap, if some other threads modifies them concurrently. This mostly affects LocalContextProvider that use the global runtime, i.e. ConcurrentLocalContextProvider and obviously SingletonLocalContextProvider. Such usage has always been race-y, but the detail on what constitutes the critical section has changed.
1 parent 3264fb8 commit 306ee51

10 files changed

Lines changed: 864 additions & 88 deletions
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/**
2+
* **** BEGIN LICENSE BLOCK *****
3+
* Version: EPL 2.0/GPL 2.0/LGPL 2.1
4+
*
5+
* The contents of this file are subject to the Eclipse Public
6+
* License Version 2.0 (the "License"); you may not use this file
7+
* except in compliance with the License. You may obtain a copy of
8+
* the License at http://www.eclipse.org/legal/epl-v20.html
9+
*
10+
* Software distributed under the License is distributed on an "AS
11+
* IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
12+
* implied. See the License for the specific language governing
13+
* rights and limitations under the License.
14+
*
15+
* Copyright (C) 2011 Yoko Harada <yokolet@gmail.com>
16+
*
17+
* Alternatively, the contents of this file may be used under the terms of
18+
* either of the GNU General Public License Version 2 or later (the "GPL"),
19+
* or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
20+
* in which case the provisions of the GPL or the LGPL are applicable instead
21+
* of those above. If you wish to allow use of your version of this file only
22+
* under the terms of either the GPL or the LGPL, and not to allow others to
23+
* use your version of this file under the terms of the EPL, indicate your
24+
* decision by deleting the provisions above and replace them with the notice
25+
* and other provisions required by the GPL or the LGPL. If you do not delete
26+
* the provisions above, a recipient may use your version of this file under
27+
* the terms of any one of the EPL, the GPL or the LGPL.
28+
* **** END LICENSE BLOCK *****
29+
*/
30+
package org.jruby.embed.internal;
31+
32+
import java.util.Collection;
33+
import java.util.Set;
34+
35+
import org.jruby.RubyArray;
36+
import org.jruby.embed.variable.BiVariable.Type;
37+
import org.jruby.runtime.builtin.IRubyObject;
38+
39+
/**
40+
*
41+
* @author yoko
42+
*/
43+
class ArgvAccessor extends VariableAccessor {
44+
private static final String NAME = "ARGV";
45+
46+
public ArgvAccessor() {
47+
super(NAME, false, Type.Argv);
48+
}
49+
50+
@Override
51+
Set<String> keySet(BiVariableMap map, IRubyObject receiver) {
52+
return Set.of(NAME);
53+
}
54+
55+
@Override
56+
IRubyObject retrieve(BiVariableMap map, IRubyObject receiver, String name) {
57+
return map.getRuntime().getTopSelf().getMetaClass().getConstant(NAME);
58+
}
59+
60+
@Override
61+
void inject(BiVariableMap map, IRubyObject receiver, String name, IRubyObject value) {
62+
// FIXME cast string[] and lists to RubyArray
63+
// FIXME probably simply need to reject non-arrays here
64+
if (value instanceof RubyArray)
65+
map.getRuntime().getTopSelf().getMetaClass().storeConstant(NAME, value);
66+
/*
67+
* FIXME straight-forward except for weird casting of the java object: it gets
68+
* converted to an array if possible... or just ignored if not. more precisely:
69+
* Collection and String[] are converted to a ruby array (of string for
70+
* String[], and of potentially weird things for Collection). everything else is
71+
* silently turned into an empty array, no warnings or anything :/
72+
*/
73+
map.getRuntime().getConstantInvalidator(NAME).invalidate();
74+
}
75+
76+
@Override
77+
void remove(BiVariableMap map, IRubyObject receiver, String name) {
78+
// cannot remove ARGV, so we just set it to an empty list instead
79+
inject(map, receiver, name, RubyArray.newArray(map.getRuntime()));
80+
}
81+
}

‎core/src/main/java/org/jruby/embed/internal/BiVariableMap.java‎

Lines changed: 134 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
import java.util.ArrayList;
3737
import java.util.Collection;
3838
import java.util.HashMap;
39-
import java.util.LinkedHashSet;
39+
import java.util.HashSet;
4040
import java.util.List;
4141
import java.util.Map;
4242
import java.util.Set;
@@ -46,6 +46,7 @@
4646
import org.jruby.embed.LocalVariableBehavior;
4747
import org.jruby.embed.variable.BiVariable;
4848
import org.jruby.embed.variable.VariableInterceptor;
49+
import org.jruby.javasupport.JavaEmbedUtils;
4950
import org.jruby.runtime.DynamicScope;
5051
import org.jruby.runtime.builtin.IRubyObject;
5152

@@ -76,6 +77,7 @@ public class BiVariableMap implements Map<String, Object> {
7677

7778
private List<String> varNames;
7879
private List<BiVariable> variables;
80+
private Map<String, IRubyObject> localVariables;
7981

8082
/**
8183
* Constructs an empty map. Users do not instantiate this map. The map is created
@@ -173,11 +175,19 @@ private static String checkKey(final Object key) {
173175
* @param key is a key to be tested its presence
174176
* @return <code>true</code> if this map contains a mapping for the specified key
175177
*/
176-
@Override
177-
public boolean containsKey(final Object key) {
178-
if ( varNames == null || key == null ) return false;
179-
return varNames.contains( checkKey(key) );
180-
}
178+
@Override
179+
public boolean containsKey(final Object key) {
180+
return containsKey(null, key);
181+
}
182+
183+
public boolean containsKey(final Object receiver, final Object key) {
184+
final String name = checkKey(key);
185+
final VariableAccessor acc = VariableAccessor.getInstance(getLocalVariableBehavior(), name);
186+
if (acc == null)
187+
return false;
188+
final IRubyObject rec = getReceiver(receiver);
189+
return acc.containsKey(this, rec, name);
190+
}
181191

182192
/**
183193
* Returns <code>true</code> if this map maps one or more keys to the
@@ -219,20 +229,26 @@ public Object get(Object key) {
219229
* @return the value in simple Java object to which the specified key is mapped, or
220230
* {@code null} if this map contains no mapping for the key
221231
*/
222-
public Object get(Object receiver, Object key) {
223-
checkKey(key);
224-
final RubyObject robj = getReceiverObject(receiver);
225-
// attemps to retrieve global variables
226-
if ( isLazy() ) {
227-
VariableInterceptor.tryLazyRetrieval(context.getLocalVariableBehavior(), this, robj, key);
228-
}
229-
BiVariable var = getVariable(robj, (String) key);
230-
return var == null ? null : var.getJavaObject();
231-
}
232-
233-
private RubyObject getReceiverObject(final Object receiver) {
234-
return receiver instanceof RubyObject ? (RubyObject) receiver : getTopSelf();
235-
}
232+
public Object get(Object receiver, Object key) {
233+
final String name = checkKey(key);
234+
final VariableAccessor acc = VariableAccessor.getInstance(getLocalVariableBehavior(), name);
235+
if (acc == null)
236+
return null;
237+
final IRubyObject rec = getReceiver(receiver);
238+
return toJava(acc.retrieve(this, rec, name));
239+
}
240+
241+
private Object toJava(final IRubyObject value) {
242+
return value != null ? value.toJava(Object.class) : null;
243+
}
244+
245+
private IRubyObject getReceiver(final Object receiver) {
246+
if (receiver == null)
247+
return null;
248+
if (receiver instanceof IRubyObject rec)
249+
return rec;
250+
throw new IllegalArgumentException("receiver object is not a Ruby object");
251+
}
236252

237253
private RubyObject getTopSelf() {
238254
return (RubyObject) getRuntime().getTopSelf();
@@ -324,66 +340,94 @@ public Object put(String key, Object value) {
324340
* @return the previous value associated with <code>key</code>, or
325341
* <code>null</code> if there was no mapping for <code>key</code>.
326342
*/
327-
public Object put(Object receiver, String key, Object value) {
328-
checkKey(key);
329-
final RubyObject robj = getReceiverObject(receiver);
330-
BiVariable var = getVariable(robj, key);
331-
Object oldValue = null;
332-
if ( var != null ) { // updates
333-
oldValue = var.getJavaObject();
334-
var.setJavaObject(robj.getRuntime(), value);
335-
}
336-
else { // creates new value
337-
var = VariableInterceptor.getVariableInstance(context.getLocalVariableBehavior(), robj, key, value);
338-
if ( var != null ) update(key, var);
339-
}
340-
return oldValue;
341-
}
343+
public Object put(Object receiver, String key, Object value) {
344+
final String name = checkKey(key);
345+
final VariableAccessor acc = VariableAccessor.getInstance(getLocalVariableBehavior(), name);
346+
if (acc == null)
347+
return null;
348+
final IRubyObject rec = getReceiver(receiver);
349+
final IRubyObject prevValue = acc.retrieve(this, rec, name);
350+
final IRubyObject rubyValue = value instanceof IRubyObject ? (IRubyObject) value
351+
: JavaEmbedUtils.javaToRuby(getRuntime(), value);
352+
acc.inject(this, rec, name, rubyValue);
353+
return toJava(prevValue);
354+
}
342355

343356
/**
344357
* Returns Ruby's local variable names this map has. The returned array is mainly
345358
* used to inject local variables to Ruby scripts while parsing.
346359
*
347360
* @return String array of Ruby's local variable names
348361
*/
349-
public String[] getLocalVarNames() {
350-
if ( variables == null ) return EMPTY_STRING_ARRAY;
351-
352-
List<String> localVarNames = new ArrayList<>(variables.size());
353-
for ( final BiVariable var : variables ) {
354-
if ( var.getType() == BiVariable.Type.LocalVariable ) {
355-
localVarNames.add( var.getName() );
356-
}
357-
}
358-
return localVarNames.toArray(new String[localVarNames.size()]);
359-
}
360-
362+
public String[] getLocalVarNames() {
363+
if (localVariables == null)
364+
return EMPTY_STRING_ARRAY;
365+
return localVariables.keySet().toArray(String[]::new);
366+
}
367+
368+
Set<String> getLocalVariableNames() {
369+
if (localVariables == null)
370+
return Set.of();
371+
return localVariables.keySet();
372+
}
373+
374+
IRubyObject getLocalVariable(final String name) {
375+
if (localVariables == null)
376+
return null;
377+
return localVariables.get(name);
378+
}
379+
380+
void setLocalVariable(final String name, final IRubyObject value) {
381+
if (localVariables == null)
382+
localVariables = new HashMap<String, IRubyObject>();
383+
localVariables.put(name, value);
384+
}
385+
386+
void removeLocalVariable(final String name) {
387+
if (localVariables != null)
388+
localVariables.remove(name);
389+
}
390+
361391
/**
362392
* Returns Ruby's local variable values this map has. The returned array is
363393
* mainly used to inject local variables to Ruby scripts while evaluating.
364394
*
365395
* @return IRubyObject array of Ruby's local variable names.
366396
*/
367-
public IRubyObject[] getLocalVarValues() {
368-
if ( variables == null ) return IRubyObject.NULL_ARRAY;
369-
370-
List<IRubyObject> localVarValues = new ArrayList<>(variables.size());
371-
for ( final BiVariable var : variables ) {
372-
if ( var.getType() == BiVariable.Type.LocalVariable ) {
373-
localVarValues.add( var.getRubyObject() );
374-
}
375-
}
376-
return localVarValues.toArray( new IRubyObject[ localVarValues.size() ] );
377-
}
378-
379-
void inject(final DynamicScope scope) {
380-
VariableInterceptor.inject(this, scope);
381-
}
382-
383-
void retrieve(final IRubyObject receiver) {
384-
final RubyObject robj = getReceiverObject(receiver);
385-
VariableInterceptor.retrieve(getLocalVariableBehavior(), this, robj);
386-
}
397+
@Deprecated
398+
public IRubyObject[] getLocalVarValues() {
399+
if (localVariables == null)
400+
return IRubyObject.NULL_ARRAY;
401+
return localVariables.values().toArray(IRubyObject[]::new);
402+
}
403+
404+
void inject(final DynamicScope scope) {
405+
if (localVariables == null)
406+
return;
407+
final String[] names = scope.getAllNamesInScope();
408+
for (int i = 0; i < names.length; i++)
409+
if (localVariables.containsKey(names[i]))
410+
scope.setValue(i, localVariables.get(names[i]), 0);
411+
}
412+
413+
void retrieve(final IRubyObject receiver) {
414+
if (persistLocalVariables(getLocalVariableBehavior())) {
415+
final DynamicScope scope = (DynamicScope) receiver.getRuntime().getCurrentContext().getCurrentScope();
416+
final String[] names = scope.getAllNamesInScope();
417+
for (int i = 0; i < names.length; i++)
418+
setLocalVariable(names[i], scope.getValue(i, 0));
419+
}
420+
}
421+
422+
private boolean persistLocalVariables(LocalVariableBehavior localVariableBehavior) {
423+
switch (getLocalVariableBehavior()) {
424+
case BSF:
425+
case PERSISTENT:
426+
return true;
427+
default:
428+
return false;
429+
}
430+
}
387431

388432
void terminate() {
389433
VariableInterceptor.terminateGlobalVariables(getLocalVariableBehavior(), getVariables(), getRuntime());
@@ -402,7 +446,7 @@ void terminate() {
402446
*/
403447
@Override
404448
public Object remove(final Object key) {
405-
return removeFrom(getTopSelf(), key);
449+
return removeFrom(null, key);
406450
}
407451

408452
/**
@@ -416,22 +460,16 @@ public Object remove(final Object key) {
416460
* @return the previous value associated with <code>key</code>, or
417461
* <code>null</code> if there was no mapping for <code>key</code>.
418462
*/
419-
public Object removeFrom(final Object receiver, final Object key) {
420-
if ( variables == null ) return null;
421-
checkKey(key);
422-
final RubyObject robj = getReceiverObject(receiver);
423-
for ( int i = 0; i < size(); i++ ) {
424-
if ( key.equals( varNames.get(i) ) ) {
425-
final BiVariable var = variables.get(i);
426-
if ( var.isReceiverIdentical(robj) ) {
427-
varNames.remove(i);
428-
variables.remove(i);
429-
return var.getJavaObject();
430-
}
431-
}
432-
}
433-
return null;
434-
}
463+
public Object removeFrom(final Object receiver, final Object key) {
464+
final String name = checkKey(key);
465+
final VariableAccessor acc = VariableAccessor.getInstance(getLocalVariableBehavior(), name);
466+
if (acc == null)
467+
return null;
468+
final IRubyObject rec = getReceiver(receiver);
469+
final IRubyObject prevValue = acc.retrieve(this, rec, name);
470+
acc.remove(this, rec, name);
471+
return toJava(prevValue);
472+
}
435473

436474
/**
437475
* Copies all of the mappings from the specified map to this map.
@@ -488,10 +526,18 @@ public void clear() {
488526
*
489527
* @return a set view of the keys contained in this map
490528
*/
491-
@Override
492-
public Set<String> keySet() {
493-
return new LinkedHashSet<String>( getNames() );
494-
}
529+
@Override
530+
public Set<String> keySet() {
531+
return keySet(null);
532+
}
533+
534+
public Set<String> keySet(final Object receiver) {
535+
final IRubyObject rec = getReceiver(receiver);
536+
final Set<String> names = new HashSet<>();
537+
for (final VariableAccessor acc : VariableAccessor.getAll(getLocalVariableBehavior()))
538+
names.addAll(acc.keySet(this, rec));
539+
return names;
540+
}
495541

496542
/**
497543
* Returns a {@link Collection} view of the values contained in this map.

0 commit comments

Comments
 (0)