/*
 ***** BEGIN LICENSE BLOCK *****
 * Version: EPL 2.0/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Eclipse Public
 * License Version 2.0 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a copy of
 * the License at http://www.eclipse.org/legal/epl-v20.html
 *
 * Software distributed under the License is distributed on an "AS
 * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * rights and limitations under the License.
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either of the GNU General Public License Version 2 or later (the "GPL"),
 * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the EPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the EPL, the GPL or the LGPL.
 ***** END LICENSE BLOCK *****/

package org.jruby.runtime.ivars;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.invoke.MethodHandle;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import org.jruby.Ruby;
import org.jruby.RubyBasicObject;
import org.jruby.RubyClass;
import org.jruby.runtime.ObjectSpace;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.util.ArraySupport;
import org.jruby.util.cli.Options;
import org.jruby.util.unsafe.UnsafeHolder;
import static org.jruby.util.StringSupport.EMPTY_STRING_ARRAY;

This class encapculates all logic relating to the management of instance variable tables in RubyBasicObject instances. The logic originally lived in both RubyBasicObject and RubyClass, tightly coupled to each and difficult to follow as it bounced back and forth. We moved the logic here for a couple reasons:
  • To have a single place from which we could follow ivar logic.
  • To make it easier to swap in new implementations of instance variable logic as we work toward reifying ivars into fields.
  • To remove rather noisy logic from RubyBasicObject and RubyClass.
  • /** * This class encapculates all logic relating to the management of instance * variable tables in RubyBasicObject instances. * * The logic originally lived in both RubyBasicObject and RubyClass, tightly * coupled to each and difficult to follow as it bounced back and forth. We * moved the logic here for a couple reasons: * * <li>To have a single place from which we could follow ivar logic.</li> * <li>To make it easier to swap in new implementations of instance variable * logic as we work toward reifying ivars into fields.</li> * <li>To remove rather noisy logic from RubyBasicObject and RubyClass.</li> */
    public class VariableTableManager {
    the "real" class associated with this table
    /** the "real" class associated with this table */
    private final RubyClass realClass;
    a map from strings to accessors for this table
    /** a map from strings to accessors for this table */
    @SuppressWarnings("unchecked") private Map<String, VariableAccessor> variableAccessors = Collections.EMPTY_MAP;
    an array of all registered variable names
    /** an array of all registered variable names */
    private volatile String[] variableNames = EMPTY_STRING_ARRAY;
    whether a slot has been allocated to object_id
    /** whether a slot has been allocated to object_id */
    private volatile int hasObjectID = 0;
    whether a slot has been allocated to ffi
    /** whether a slot has been allocated to ffi */
    private volatile int hasFFI = 0;
    whether a slot has been allocated to objectspace_group
    /** whether a slot has been allocated to objectspace_group */
    private volatile int hasObjectspaceGroup = 0;
    whether objects associated with this table use fields
    /** whether objects associated with this table use fields */
    private volatile int fieldVariables = 0;
    a lazy accessor for object_id
    /** a lazy accessor for object_id */
    private final VariableAccessorField objectIdVariableAccessorField = new VariableAccessorField("object_id");
    a lazy accessor for FFI handle
    /** a lazy accessor for FFI handle */
    private final VariableAccessorField ffiHandleVariableAccessorField = new VariableAccessorField("ffi");
    a lazy accessor for object group
    /** a lazy accessor for object group */
    private final VariableAccessorField objectGroupVariableAccessorField = new VariableAccessorField("objectspace_group");
    Construct a new VariableTable Manager for the given "real" class.
    Params:
    • realClass – the "real" class associated with this table
    /** * Construct a new VariableTable Manager for the given "real" class. * * @param realClass the "real" class associated with this table */
    public VariableTableManager(RubyClass realClass) { this.realClass = realClass; }
    Get the map of all current variable accessors with intent to read from it.
    Returns:a map of current variable accessors
    /** * Get the map of all current variable accessors with intent to read from it. * * @return a map of current variable accessors */
    public Map<String, VariableAccessor> getVariableAccessorsForRead() { return variableAccessors; }
    Whether this table has been used to allocate space for an object_id.
    Returns:true if object_id has been allocated; false otherwise
    /** * Whether this table has been used to allocate space for an object_id. * * @return true if object_id has been allocated; false otherwise */
    public boolean hasObjectID() { return hasObjectID == 1; }
    Get the object_id from a given RubyBasicObject, possibly allocating space for it.
    Params:
    • self – the object from which to get object_id
    Returns:the object's object_id (possibly new)
    /** * Get the object_id from a given RubyBasicObject, possibly allocating * space for it. * * @param self the object from which to get object_id * @return the object's object_id (possibly new) */
    public long getObjectId(RubyBasicObject self) { VariableAccessor objectIdAccessor = getObjectIdAccessorForRead(); Long id = (Long)objectIdAccessor.get(self); if (id != null) return id; synchronized (self) { objectIdAccessor = getObjectIdAccessorForRead(); id = (Long)objectIdAccessor.get(self); if (id != null) return id; objectIdAccessor = getObjectIdAccessorForWrite(); return initObjectId(self, objectIdAccessor); } }
    Virtual entry point for setting a variable into an object.
    Params:
    • self – the object into which to set the value
    • index – the index allocated for the value
    • value – the value
    /** * Virtual entry point for setting a variable into an object. * * @param self the object into which to set the value * @param index the index allocated for the value * @param value the value */
    public void setVariableInternal(RubyBasicObject self, int index, Object value) { if(UnsafeHolder.U == null) { SynchronizedVariableAccessor.setVariable(self,realClass,index,value); } else { StampedVariableAccessor.setVariable(self,realClass,index,value); } }
    Static entry point for setting a variable in an object.
    Params:
    • realClass – the "real" class of the object
    • self – the object into which to set the variable
    • index – the index allocated for the variable
    • value – the value of the variable
    /** * Static entry point for setting a variable in an object. * * @param realClass the "real" class of the object * @param self the object into which to set the variable * @param index the index allocated for the variable * @param value the value of the variable */
    public static void setVariableInternal(RubyClass realClass, RubyBasicObject self, int index, Object value) { if(UnsafeHolder.U == null) { SynchronizedVariableAccessor.setVariable(self,realClass,index,value); } else { StampedVariableAccessor.setVariable(self,realClass,index,value); } }
    Get the variable accessor for the given name with intent to use it for writing.
    Params:
    • name – the name of the variable
    Returns:an accessor appropriate for writing
    /** * Get the variable accessor for the given name with intent to use it for * writing. * * @param name the name of the variable * @return an accessor appropriate for writing */
    public VariableAccessor getVariableAccessorForWrite(String name) { VariableAccessor ivarAccessor = variableAccessors.get(name); if (ivarAccessor == null) { synchronized (realClass) { Map<String, VariableAccessor> myVariableAccessors = variableAccessors; ivarAccessor = myVariableAccessors.get(name); if (ivarAccessor == null) { // allocate a new accessor and populate a new table ivarAccessor = allocateVariableAccessor(name); Map<String, VariableAccessor> newVariableAccessors = new HashMap<String, VariableAccessor>(myVariableAccessors.size() + 1); newVariableAccessors.putAll(myVariableAccessors); newVariableAccessors.put(name, ivarAccessor); variableAccessors = newVariableAccessors; } } } return ivarAccessor; } public VariableAccessor getVariableAccessorForVar(String name, MethodHandle getter, MethodHandle setter) { VariableAccessor ivarAccessor = variableAccessors.get(name); if (ivarAccessor == null) { synchronized (realClass) { Map<String, VariableAccessor> myVariableAccessors = variableAccessors; ivarAccessor = myVariableAccessors.get(name); if (ivarAccessor == null) { // allocate a new accessor and populate a new table ivarAccessor = allocateVariableAccessorForVar(name, getter, setter); Map<String, VariableAccessor> newVariableAccessors = new HashMap<String, VariableAccessor>(myVariableAccessors.size() + 1); newVariableAccessors.putAll(myVariableAccessors); newVariableAccessors.put(name, ivarAccessor); variableAccessors = newVariableAccessors; } } } return ivarAccessor; }
    Get the variable accessor for the given name with intent to use it for reading.
    Params:
    • name – the name of the variable
    Returns:an accessor appropriate for reading
    /** * Get the variable accessor for the given name with intent to use it for * reading. * * @param name the name of the variable * @return an accessor appropriate for reading */
    public VariableAccessor getVariableAccessorForRead(String name) { VariableAccessor accessor = getVariableAccessorsForRead().get(name); if (accessor == null) accessor = VariableAccessor.DUMMY_ACCESSOR; return accessor; }
    Retrieve the read accessor for object_id for reads. If no object_id has been prepared, this will return a dummy accessor that just returns null.
    Returns:the read accessor for object_id
    /** * Retrieve the read accessor for object_id for reads. If no object_id has been prepared, this will return a dummy * accessor that just returns null. * * @return the read accessor for object_id */
    public VariableAccessor getObjectIdAccessorForRead() { return objectIdVariableAccessorField.getVariableAccessorForRead(); }
    Retrieve the write accessor for object_id.
    Returns:the write accessor for object_id
    /** * Retrieve the write accessor for object_id. * * @return the write accessor for object_id */
    public VariableAccessor getObjectIdAccessorForWrite() { if (hasObjectID == 0) hasObjectID = 1; return objectIdVariableAccessorField.getVariableAccessorForWrite(this); }
    Retrieve the read accessor for FFI handle. If no object_id has been prepared, this will return a dummy accessor that just returns null.
    Returns:the read accessor for FFI handle
    /** * Retrieve the read accessor for FFI handle. If no object_id has been prepared, this will return a dummy * accessor that just returns null. * * @return the read accessor for FFI handle */
    public VariableAccessor getFFIHandleAccessorForRead() { return ffiHandleVariableAccessorField.getVariableAccessorForRead(); }
    Retrieve the write accessor for FFI handle.
    Returns:the write accessor for FFI handle
    /** * Retrieve the write accessor for FFI handle. * * @return the write accessor for FFI handle */
    public VariableAccessor getFFIHandleAccessorForWrite() { if (hasFFI == 0) hasFFI = 1; return ffiHandleVariableAccessorField.getVariableAccessorForWrite(this); }
    Retrieve the read accessor for object group. If no object_id has been prepared, this will return a dummy accessor that just returns null.
    Returns:the read accessor for object group
    /** * Retrieve the read accessor for object group. If no object_id has been prepared, this will return a dummy * accessor that just returns null. * * @return the read accessor for object group */
    public VariableAccessor getObjectGroupAccessorForRead() { return objectGroupVariableAccessorField.getVariableAccessorForRead(); }
    Retrieve the write accessor for object group.
    Returns:the write accessor for object group
    /** * Retrieve the write accessor for object group. * * @return the write accessor for object group */
    public VariableAccessor getObjectGroupAccessorForWrite() { if (hasObjectspaceGroup == 0) hasObjectspaceGroup = 1; return objectGroupVariableAccessorField.getVariableAccessorForWrite(this); }
    Retrieve the FFI ext handle for the given object.
    Params:
    • self – the object
    Returns:the object's FFI handle
    /** * Retrieve the FFI ext handle for the given object. * * @param self the object * @return the object's FFI handle */
    public final Object getFFIHandle(RubyBasicObject self) { return getFFIHandleAccessorForRead().get(self); }
    Set the FFI handle for the given object.
    Params:
    • self – the object
    • self – the object's FFI handle
    /** * Set the FFI handle for the given object. * * @param self the object * @param self the object's FFI handle */
    public final void setFFIHandle(RubyBasicObject self, Object value) { int index = getFFIHandleAccessorForWrite().getIndex(); setVariableInternal(realClass, self, index, value); }
    Get the size of the variable table, excluding extra vars (object_id, etc).
    Returns:the variable table's size, excluding extras
    /** * Get the size of the variable table, excluding extra vars (object_id, * etc). * * @return the variable table's size, excluding extras */
    public int getVariableTableSize() { return variableAccessors.size(); }
    Get the size of the variable table, including extra vars (object_etc, etc).
    Returns:
    /** * Get the size of the variable table, including extra vars (object_etc, * etc). * * @return */
    public int getVariableTableSizeWithExtras() { return variableNames.length; }
    Get a Map representing all variables registered in the variable table.
    Returns:a map of names to accessors for all variables
    /** * Get a Map representing all variables registered in the variable table. * * @return a map of names to accessors for all variables */
    public Map<String, VariableAccessor> getVariableTableCopy() { return new HashMap<String, VariableAccessor>(getVariableAccessorsForRead()); }
    Get an array of all the known instance variable names. The offset into the array indicates the offset of the variable's value in the per-object variable array.
    Returns:a copy of the array of known instance variable names
    /** * Get an array of all the known instance variable names. The offset into * the array indicates the offset of the variable's value in the per-object * variable array. * * @return a copy of the array of known instance variable names */
    public String[] getVariableNames() { return variableNames.clone(); }
    Sync one this object's variables with other's - this is used to make rbClone work correctly.
    Params:
    • self – the object into which to sync variables
    • other – the object from which to sync variables
    /** * Sync one this object's variables with other's - this is used to make * rbClone work correctly. * * @param self the object into which to sync variables * @param other the object from which to sync variables */
    public void syncVariables(RubyBasicObject self, IRubyObject other) { RubyClass otherRealClass = other.getMetaClass().getRealClass(); boolean sameTable = otherRealClass == realClass; if (sameTable && fieldVariables == 0) { int idIndex = otherRealClass.getObjectIdAccessorField().getVariableAccessorForRead().getIndex(); Object[] otherVars = ((RubyBasicObject) other).varTable; if(UnsafeHolder.U == null) { synchronized (self) { self.varTable = makeSyncedTable(self.varTable, otherVars, idIndex); } } else { for(;;) { int oldStamp = self.varTableStamp; // wait for read mode if((oldStamp & 0x01) == 1) continue; // acquire exclusive write mode if(!UnsafeHolder.U.compareAndSwapInt(self, RubyBasicObject.STAMP_OFFSET, oldStamp, ++oldStamp)) continue; Object[] currentTable = (Object[]) UnsafeHolder.U.getObjectVolatile(self, RubyBasicObject.VAR_TABLE_OFFSET); Object[] newTable = makeSyncedTable(currentTable,otherVars, idIndex); UnsafeHolder.U.putOrderedObject(self, RubyBasicObject.VAR_TABLE_OFFSET, newTable); // release write mode self.varTableStamp = oldStamp+1; break; } } } else { for (Map.Entry<String, VariableAccessor> entry : otherRealClass.getVariableAccessorsForRead().entrySet()) { VariableAccessor accessor = entry.getValue(); Object value = accessor.get(other); if (value != null) { if (sameTable) { accessor.set(self, value); } else { realClass.getVariableAccessorForWrite(accessor.getName()).set(self, value); } } } } }
    Returns true if object has any variables, defined as:
    • instance variables
    • class variables
    • constants
    • internal variables, such as those used when marshaling Ranges and Exceptions
    Returns:true if object has any variables, else false
    /** * Returns true if object has any variables, defined as: * <ul> * <li> instance variables * <li> class variables * <li> constants * <li> internal variables, such as those used when marshaling Ranges and Exceptions * </ul> * @return true if object has any variables, else false */
    public boolean hasVariables(RubyBasicObject object) { // we check both to exclude object_id Object[] myVarTable; return fieldVariables > 0 || (myVarTable = object.varTable) != null && myVarTable.length > hasObjectID + hasFFI + hasObjectspaceGroup; } public void serializeVariables(RubyBasicObject object, ObjectOutputStream oos) throws IOException { if (object.varTable != null) { Map<String, VariableAccessor> accessors = getVariableAccessorsForRead(); oos.writeInt(accessors.size()); for (VariableAccessor accessor : accessors.values()) { oos.writeUTF(RubyBasicObject.ERR_INSECURE_SET_INST_VAR); oos.writeObject(accessor.get(object)); } } else { oos.writeInt(0); } } public void deserializeVariables(RubyBasicObject object, ObjectInputStream ois) throws IOException, ClassNotFoundException { int varCount = ois.readInt(); for (int i = 0; i < varCount; i++) { String name = ois.readUTF(); Object value = ois.readObject(); getVariableAccessorForWrite(name).set(object, value); } } public Object clearVariable(RubyBasicObject object, String name) { synchronized(object) { Object value = getVariableAccessorForRead(name).get(object); getVariableAccessorForWrite(name).set(object, null); return value; } }
    We lazily stand up the object ID since it forces us to stand up per-object state for a given object. We also check for ObjectSpace here, and normally we do not register a given object ID into ObjectSpace due to the high cost associated with constructing the related weakref. Most uses of id/object_id will only ever need it to be a unique identifier, and the id2ref behavior provided by ObjectSpace is considered internal and not generally supported. Note that this method does not need to be synchronized, since it is only called from #getObjectId, which synchronizes against the target object. The accessor is already present, and variable accesses are thread-safe (albeit not atomic, which necessitates the synchronization in getObjectId). Synchronization here ends up being a bottleneck for every object created from the class that contains this VariableTableManager. See GH #1400.
    Params:
    • objectIdAccessor – The variable accessor to use for storing the generated object ID
    Returns:The generated object ID
    /** * We lazily stand up the object ID since it forces us to stand up * per-object state for a given object. We also check for ObjectSpace here, * and normally we do not register a given object ID into ObjectSpace due * to the high cost associated with constructing the related weakref. Most * uses of id/object_id will only ever need it to be a unique identifier, * and the id2ref behavior provided by ObjectSpace is considered internal * and not generally supported. * * Note that this method does not need to be synchronized, since it is * only called from #getObjectId, which synchronizes against the target * object. The accessor is already present, and variable accesses are * thread-safe (albeit not atomic, which necessitates the synchronization * in getObjectId). * * Synchronization here ends up being a bottleneck for every object * created from the class that contains this VariableTableManager. See * GH #1400. * * @param objectIdAccessor The variable accessor to use for storing the * generated object ID * @return The generated object ID */
    private long initObjectId(RubyBasicObject self, VariableAccessor objectIdAccessor) { Ruby runtime = self.getRuntime(); long id; if (runtime.isObjectSpaceEnabled()) { id = runtime.getObjectSpace().createAndRegisterObjectId(self); } else { id = ObjectSpace.calculateObjectId(self); } // we use a direct path here to avoid frozen checks setObjectId(realClass, self, objectIdAccessor.getIndex(), id); return id; }
    Update object_id with the given value.
    Params:
    • self – the object into which to set object_id
    • index – the index allocated to store object_id
    • value – the value of object_id
    /** * Update object_id with the given value. * * @param self the object into which to set object_id * @param index the index allocated to store object_id * @param value the value of object_id */
    private static void setObjectId(RubyClass realClass, RubyBasicObject self, int index, long value) { if (index < 0) return; setVariableInternal(realClass, self, index, value); }
    Make a new variable table based on the values in a current and other (incoming) table, excluding object_id at specified index.
    Params:
    • currentTable – the current table
    • otherTable – the other (incoming) table
    • objectIdIdx – the index of object_id to exclude
    Returns:a new table formed by combining the given tables
    /** * Make a new variable table based on the values in a current and other * (incoming) table, excluding object_id at specified index. * * @param currentTable the current table * @param otherTable the other (incoming) table * @param objectIdIdx the index of object_id to exclude * @return a new table formed by combining the given tables */
    private static Object[] makeSyncedTable(Object[] currentTable, Object[] otherTable, int objectIdIdx) { if (currentTable == null || currentTable.length < otherTable.length) { currentTable = otherTable.clone(); } else { ArraySupport.copy(otherTable, currentTable, 0, otherTable.length); } // null out object ID so we don't share it if (objectIdIdx >= 0 && objectIdIdx < currentTable.length) { currentTable[objectIdIdx] = null; } return currentTable; }
    Allocate a new VariableAccessor for the named variable.
    Params:
    • name – the name of the variable
    Returns:the new VariableAccessor
    /** * Allocate a new VariableAccessor for the named variable. * * @param name the name of the variable * @return the new VariableAccessor */
    synchronized final VariableAccessor allocateVariableAccessor(String name) { int id = realClass.id; final String[] myVariableNames = variableNames; final int newIndex = myVariableNames.length; VariableAccessor newVariableAccessor; if (Options.VOLATILE_VARIABLES.load()) { if (UnsafeHolder.U == null) { newVariableAccessor = new SynchronizedVariableAccessor(realClass, name, newIndex, id); } else { newVariableAccessor = new StampedVariableAccessor(realClass, name, newIndex, id); } } else { if (UnsafeHolder.U == null) { newVariableAccessor = new NonvolatileVariableAccessor(realClass, name, newIndex, id); } else { // We still need safe updating of the vartable, so we fall back on sync here too. newVariableAccessor = new StampedVariableAccessor(realClass, name, newIndex, id); } } final String[] newVariableNames = new String[newIndex + 1]; ArraySupport.copy(myVariableNames, 0, newVariableNames, 0, newIndex); newVariableNames[newIndex] = name; variableNames = newVariableNames; return newVariableAccessor; } synchronized final VariableAccessor allocateVariableAccessorForVar(String name, MethodHandle getter, MethodHandle setter) { int id = realClass.id; final String[] myVariableNames = variableNames; final int newIndex = myVariableNames.length; fieldVariables += 1; VariableAccessor newVariableAccessor = new FieldVariableAccessor(realClass, name, newIndex, id, getter, setter); final String[] newVariableNames = new String[newIndex + 1]; ArraySupport.copy(myVariableNames, 0, newVariableNames, 0, newIndex); newVariableNames[newIndex] = name; variableNames = newVariableNames; return newVariableAccessor; }
    Retrieve the lazy accessor (VariableAccessorField) for object_id.
    Returns:the lazy accessor for object_id
    Deprecated:Use getObjectIdAccessorForRead() or getObjectIdAccessorForWrite()
    /** * Retrieve the lazy accessor (VariableAccessorField) for object_id. * * @return the lazy accessor for object_id * @deprecated Use {@link #getObjectIdAccessorForRead()} or {@link #getObjectIdAccessorForWrite()} */
    public VariableAccessorField getObjectIdAccessorField() { return objectIdVariableAccessorField; }
    Retrieve the lazy accessor (VariableAccessorField) for FFI handle.
    Returns:the lazy accessor for FFI handle
    Deprecated:Use getFFIHandleAccessorForRead() or getFFIHandleAccessorForWrite()
    /** * Retrieve the lazy accessor (VariableAccessorField) for FFI handle. * * @return the lazy accessor for FFI handle * @deprecated Use {@link #getFFIHandleAccessorForRead()} or {@link #getFFIHandleAccessorForWrite()} */
    public VariableAccessorField getFFIHandleAccessorField() { return ffiHandleVariableAccessorField; }
    Retrieve the lazy accessor (VariableAccessorField) for object group.
    Returns:the lazy accessor for object group
    Deprecated:Use getObjectGroupAccessorForRead() or getObjectGroupAccessorForWrite()
    /** * Retrieve the lazy accessor (VariableAccessorField) for object group. * * @return the lazy accessor for object group * @deprecated Use {@link #getObjectGroupAccessorForRead()} or {@link #getObjectGroupAccessorForWrite()} */
    public VariableAccessorField getObjectGroupAccessorField() { return objectGroupVariableAccessorField; } }