Spec-Zone.ru › Ruby 4.0
  1. Ruby::
  2. Box

class Ruby::Box

Родительский класс:
Module

Ruby Box — разделение классов и модулей в процессе Ruby

Ruby Box предназначен для создания изолированных пространств в процессе Ruby, позволяющих изолировать код приложений, библиотеки и monkey patch.

Известные проблемы

  • При запуске Ruby с RUBY_BOX=1 отображается предупреждение об экспериментальном статусе (укажите параметр -W:no-experimental, чтобы скрыть его)

  • Установка нативных расширений под RUBY_BOX=1 может завершиться ошибкой из-за слишком глубокой вложенности стека в extconf.rb

  • require 'active_support/core_ext' может завершиться ошибкой под RUBY_BOX=1

  • Методы, определённые в боксе, могут быть недоступны встроенным методам, написанным на Ruby

Задачи

  • Добавить загруженный бокс в iseq, чтобы проверять, не пытается ли другой бокс выполнить этот iseq (добавлять поле, только если включён VM_CHECK_MODE?)

  • Назначить боксам собственный TOPLEVEL_BINDING

  • Исправить вызов warn в боксах, чтобы он обращался к $VERBOSE и Warning.warn в боксе

  • Сделать невидимым класс внутреннего контейнера данных Ruby::Box::Entry

  • Добавить тестовые случаи для $LOAD_PATH и $LOADED_FEATURES

Использование

Включение Ruby Box

Сначала при запуске процесса Ruby необходимо установить переменную окружения: RUBY_BOX=1. Для включения Ruby Box допустимо только значение 1. Любые другие значения (или отсутствие RUBY_BOX) отключают Ruby Box. Установка значения после запуска программы Ruby не действует.

Использование Ruby Box

Класс Ruby::Box является точкой входа Ruby Box.

box = Ruby::Box.new
box.require('something') # or require_relative, load

Требуемый файл (с расширением .rb или .so/.dll/.bundle) загружается в бокс (здесь — box). Файлы, требуемые или загружаемые из something, рекурсивно загружаются в бокс.

# something.rb

X = 1

class Something
  def self.x = X
  def x = ::X
end

Классы и модули, а также их методы и константы, определённые в боксе, доступны через объект box.

X = 2
p X                 # 2
p ::X               # 2
p box::Something.x  # 1
p box::X            # 1

Методы экземпляров, определённые в боксе, также выполняются с определениями из этого бокса.

s = box::Something.new

p s.x  # 1

Спецификация

Типы Ruby Box

Существует два типа боксов:

  • Корневой бокс

  • Пользовательские боксы

Корневой бокс — единственный бокс в процессе Ruby. Загрузка Ruby выполняется в корневом боксе, и все встроенные классы и модули определены в нём. (См. раздел «Встроенные классы и модули».)

Пользовательские боксы предназначены для выполнения программ, написанных пользователем, и библиотек, загруженных из пользовательских программ. Главная программа пользователя (указанная аргументом командной строки ruby) выполняется в боксе «main» — пользовательском боксе, автоматически созданном в конце загрузки Ruby и скопированном из корневого бокса.

При вызове Ruby::Box.new создаётся «дополнительный» бокс (пользовательский, не главный), скопированный из корневого бокса. Все пользовательские боксы являются плоскими и копируются из корневого бокса.

Класс Ruby Box и его экземпляры

Ruby::Box — это класс, подкласс Module. Экземпляры Ruby::Box являются разновидностью Module.

Классы и модули, определённые в боксах

Классы и модули, впервые определённые в боксе box, доступны через box. Например, если класс A определён в box, то за пределами бокса он доступен как box::A.

В боксе box к A можно обращаться как к A (и ::A).

Повторное открытие встроенных классов и модулей в боксах

В боксах встроенные классы и модули видны, и их можно повторно открывать. Их можно повторно открывать с помощью конструкций class или module, а определения классов и модулей можно изменять.

Изменённые определения видны только в данном боксе. В других боксах встроенные классы и модули и их экземпляры работают без этих изменений.

# in foo.rb
class String
  BLANK_PATTERN = /\A\s*\z/
  def blank?
    self =~ BLANK_PATTERN
  end
end

module Foo
  def self.foo = "foo"

  def self.foo_is_blank?
    foo.blank?
  end
end

Foo.foo.blank? #=> false
"foo".blank?   #=> false

# in main.rb
box = Ruby::Box.new
box.require('foo')

box::Foo.foo_is_blank? #=> false   (#blank? called in box)

"foo".blank?          # NoMethodError
String::BLANK_PATTERN # NameError

Бокс main и box — разные боксы, поэтому monkey patch в main также не видны в box.

Встроенные классы и модули

В контексте боксов «встроенными» называются классы и модули, которые:

  • Доступны в пользовательских скриптах без вызовов require

  • Определены до запуска любой пользовательской программы

  • Включают классы и модули, загруженные файлом prelude.rb (например, Gem RubyGems)

Далее для краткости «встроенные классы и модули» будут называться «встроенными классами».

Обращение к встроенным классам через объекты боксов

На встроенные классы в боксе box можно ссылаться из других боксов. Например, box::String — корректная ссылка, а String и box::String идентичны (String == box::String, String.object_id == box::String.object_id).

Ссылка вида box::String возвращает только String в текущем боксе, поэтому её определение — это String в боксе, а не в box.

# foo.rb
class String
  def self.foo = "foo"
end

# main.rb
box = Ruby::Box.new
box.require('foo')

box::String.foo  # NoMethodError

Переменные экземпляра класса, переменные класса и константы

Встроенные классы могут иметь разные наборы переменных экземпляра класса, переменных класса и констант в разных боксах.

# foo.rb
class Array
  @v = "foo"
  @@v = "_foo_"
  V = "FOO"
end

Array.instance_variable_get(:@v) #=> "foo"
Array.class_variable_get(:@@v)   #=> "_foo_"
Array.const_get(:V)              #=> "FOO"

# main.rb
box = Ruby::Box.new
box.require('foo')

Array.instance_variable_get(:@v) #=> nil
Array.class_variable_get(:@@v)   # NameError
Array.const_get(:V)              # NameError

Глобальные переменные

В боксах изменения глобальных переменных также изолированы. Изменения глобальных переменных в боксе видны и применяются только в этом боксе.

# foo.rb
$foo = "foo"
$VERBOSE = nil

puts "This appears: '#{$foo}'"

# main.rb
p $foo      #=> nil
p $VERBOSE  #=> false

box = Ruby::Box.new
box.require('foo')  # "This appears: 'foo'"

p $foo      #=> nil
p $VERBOSE  #=> false

Константы верхнего уровня

Обычно константы верхнего уровня определяются как константы Object. В боксах константы верхнего уровня являются константами Object в этом боксе. При этом константы объекта бокса box строго равны константам Object.

# foo.rb
FOO = 100

FOO         #=> 100
Object::FOO #=> 100

# main.rb
box = Ruby::Box.new
box.require('foo')

box::FOO      #=> 100

FOO          # NameError
Object::FOO  # NameError

Методы верхнего уровня

Методы верхнего уровня — это приватные методы экземпляров Object в каждом боксе.

# foo.rb
def yay = "foo"

class Foo
  def self.say = yay
end

Foo.say #=> "foo"
yay     #=> "foo"

# main.rb
box = Ruby::Box.new
box.require('foo')

box::Foo.say  #=> "foo"

yay  # NoMethodError

Невозможно предоставить другим боксам доступ к методам верхнего уровня. (См. раздел «Предоставление доступа к методам верхнего уровня как к методам объекта бокса» в разделе «Обсуждение» ниже.)

Области действия Ruby Box

Ruby Box работает на уровне файлов. Один файл .rb выполняется в одном боксе.

После загрузки файла в бокс box все методы и процедуры, определённые или созданные в этом файле, выполняются в box.

Вспомогательные методы

Для экспериментов и тестирования Ruby Box доступны несколько методов.

  • Ruby::Box.current возвращает текущий бокс

  • Ruby::Box.enabled? возвращает true или false в зависимости от того, указан ли RUBY_BOX=1

  • Ruby::Box.root возвращает корневой бокс

  • Ruby::Box.main возвращает бокс main

  • Ruby::Box#eval выполняет код Ruby (String) в боксе-получателе, подобно вызову load с файлом

Подробности реализации

Встроенный кэш методов и констант ISeq

Как описано выше в разделе «Области действия Ruby Box», файл «.rb» выполняется в боксе. Поэтому разрешение методов и констант всегда выполняется в одном и том же боксе.

Это означает, что встроенные кэши ISeq корректно работают и с боксами. В противном случае это ошибка.

Глобальный кэш вызовов методов (gccct)

Функция C rb_funcall() обращается к глобальной таблице кэша cc (gccct), а ключ кэша вычисляется с учётом текущего бокса.

Поэтому вызовы rb_funcall() снижают производительность, когда Ruby Box включён.

Текущий бокс и бокс загрузки

Текущий бокс — это бокс, в котором выполняется код. Ruby::Box.current возвращает объект текущего бокса.

Бокс загрузки — это бокс, управляемый внутренним образом и определяющий, в какой бокс загружать новые требуемые или загружаемые файлы. Например, box является боксом загрузки при вызове box.require("foo").

Обсуждение

Больше встроенных методов, написанных на Ruby

Если Ruby Box включён по умолчанию, встроенные методы можно писать на Ruby, поскольку пользовательские monkey patch не смогут их переопределить. Встроенные методы Ruby можно компилировать с помощью JIT, что может повысить производительность.

Monkey patch методов, вызываемых встроенными методами

Встроенные методы иногда вызывают другие встроенные методы. Например, Hash#map вызывает Hash#each, чтобы получить элементы для преобразования. Без Ruby Box пользователи Ruby могут переопределить Hash#each и рассчитывать на соответствующее изменение поведения Hash#map.

Однако с боксами Hash#map выполняется в корневом боксе. Пользователи Ruby могут определять Hash#each только в пользовательских боксах, поэтому в этом случае они не могут изменить поведение Hash#map. Чтобы добиться этого, пользователям следует переопределить и Hash#map, и Hash#each (либо только Hash#map).

Это несовместимое изменение.

Пользователи могут определять методы с помощью Ruby::Box.root.eval(...), но такой API явно неидеален.

Присваивание значений глобальным переменным, используемым встроенными методами

Как и в случае с monkey patch методов, глобальные переменные, которым присваиваются значения в боксе, отделены от переменных корневого бокса. Методы, определённые в корневом боксе и обращающиеся к глобальной переменной, не могут найти переменную с повторно присвоенным значением.

Контекст $LOAD_PATH и $LOADED_FEATURES

Глобальные переменные $LOAD_PATH и $LOADED_FEATURES определяют поведение метода require. Поэтому эти переменные определяются боксом загрузки, а не текущим боксом.

Это может противоречить ожиданиям пользователей. Необходимо найти решение.

Предоставление доступа к методам верхнего уровня как к методам объекта бокса

В настоящее время методы верхнего уровня в боксах недоступны за пределами бокса. Однако может возникнуть необходимость вызывать методы верхнего уровня другого бокса.

Разделение корневого и встроенного боксов

В настоящее время единственный «корневой» бокс является источником CoW для classext. Кроме того, «корневой» бокс может загружать дополнительные файлы после начала вычисления главного скрипта, вызывая методы, содержащие строки вроде require "openssl".

Это означает, что наборы определений в пользовательских боксах могут различаться в зависимости от времени их создания.

[root]
 |
 |----[main]
 |
 |(require "openssl" called in root)
 |
 |----[box1] having OpenSSL
 |
 |(remove_const called for OpenSSL in root)
 |
 |----[box2] without OpenSSL

Это может привести к неожиданным различиям в поведении пользовательских боксов. Это НЕ должно быть проблемой, поскольку пользовательские скрипты, обращающиеся к OpenSSL, должны самостоятельно вызывать require "openssl". Но в худшем случае скрипт (без require "openssl") успешно выполняется в box1, но не выполняется в box2. Для пользователей это выглядит как «случайный сбой».

Один из способов предотвратить эту ситуацию — использовать «корневой» и «встроенный» боксы.

  • корневой

  • Бокс для загрузки процесса Ruby, являющийся источником CoW

  • После запуска главного бокса в этом боксе не выполняется никакой код

  • встроенный

  • Бокс, скопированный из корневого одновременно с боксом «main»

  • Методы и процедуры, определённые в «корневом» боксе, выполняются в этом боксе

  • Требуемые классы и модули загружаются в этот бокс

Такая конструкция обеспечивает единый и согласованный источник CoW для боксов.

Разделение cc_tbl и callable_m_tbl, cvc_tbl для уменьшения числа операций CoW для classext

Поля rb_classext_t содержат несколько кэшей и подобных им данных: cc_tbl (таблица callcache), callable_m_tbl (таблица разрешённых дополненных методов) и cvc_tbl (таблица кэша переменных класса).

CoW для classext запускается при изменении содержимого rb_classext_t, включая cc_tbl, callable_m_tbl и cvc_tbl. Однако эти три таблицы изменяются просто при вызове методов или обращении к переменным класса. Поэтому сейчас CoW для classext запускается намного чаще, чем ожидалось изначально.

Если вынести эти три таблицы за пределы rb_classext_t, число копирований rb_classext_t значительно уменьшится по сравнению с текущей реализацией.

Общедоступные методы класса

Ruby::Box.current → box, nil or false Показать исходный код
static VALUE
rb_box_s_current(VALUE recv)
{
    const rb_box_t *box;

    if (!rb_box_available())
        return Qnil;

    box = rb_vm_current_box(GET_EC());
    VM_ASSERT(box && box->box_object);
    return box->box_object;
}

Возвращает текущий бокс. Если Ruby Box не включён, возвращает nil.

Ruby::Box.enabled? → true or false Показать исходный код
static VALUE
rb_box_s_getenabled(VALUE recv)
{
    return RBOOL(rb_box_available());
}

Возвращает true, если Ruby::Box включён.

Ruby::Box.new → new_box Показать исходный код
static VALUE
box_initialize(VALUE box_value)
{
    rb_box_t *box;
    rb_classext_t *object_classext;
    VALUE entry;
    ID id_box_entry;
    CONST_ID(id_box_entry, "__box_entry__");

    if (!rb_box_available()) {
        rb_raise(rb_eRuntimeError, "Ruby Box is disabled. Set RUBY_BOX=1 environment variable to use Ruby::Box.");
    }

    entry = rb_class_new_instance_pass_kw(0, NULL, rb_cBoxEntry);
    box = get_box_struct_internal(entry);

    box->box_object = box_value;
    box->box_id = box_generate_id();
    rb_define_singleton_method(box->load_path, "resolve_feature_path", rb_resolve_feature_path, 1);

    // Set the Ruby::Box object unique/consistent from any boxes to have just single
    // constant table from any view of every (including main) box.
    // If a code in the box adds a constant, the constant will be visible even from root/main.
    RCLASS_SET_PRIME_CLASSEXT_WRITABLE(box_value, true);

    // Get a clean constant table of Object even by writable one
    // because ns was just created, so it has not touched any constants yet.
    object_classext = RCLASS_EXT_WRITABLE_IN_BOX(rb_cObject, box);
    RCLASS_SET_CONST_TBL(box_value, RCLASSEXT_CONST_TBL(object_classext), true);

    rb_ivar_set(box_value, id_box_entry, entry);

    return box_value;
}

Возвращает новый объект Ruby::Box.

Общедоступные методы экземпляра

eval (p1) Показать исходный код
static VALUE
rb_box_eval(VALUE box_value, VALUE str)
{
    const rb_iseq_t *iseq;
    const rb_box_t *box;

    StringValue(str);

    iseq = rb_iseq_compile_iseq(str, rb_str_new_cstr("eval"));
    VM_ASSERT(iseq);

    box = (const rb_box_t *)rb_get_box_t(box_value);

    return rb_iseq_eval(iseq, box);
}
inspect () Показать исходный код
static VALUE
rb_box_inspect(VALUE obj)
{
    rb_box_t *box;
    VALUE r;
    if (obj == Qfalse) {
        r = rb_str_new_cstr("#<Ruby::Box:root>");
        return r;
    }
    box = rb_get_box_t(obj);
    r = rb_str_new_cstr("#<Ruby::Box:");
    rb_str_concat(r, rb_funcall(LONG2NUM(box->box_id), rb_intern("to_s"), 0));
    if (BOX_ROOT_P(box)) {
        rb_str_cat_cstr(r, ",root");
    }
    if (BOX_USER_P(box)) {
        rb_str_cat_cstr(r, ",user");
    }
    if (BOX_MAIN_P(box)) {
        rb_str_cat_cstr(r, ",main");
    }
    else if (BOX_OPTIONAL_P(box)) {
        rb_str_cat_cstr(r, ",optional");
    }
    rb_str_cat_cstr(r, ">");
    return r;
}
load (p1, p2 = v2) Показать исходный код
static VALUE
rb_box_load(int argc, VALUE *argv, VALUE box)
{
    VALUE fname, wrap;
    rb_scan_args(argc, argv, "11", &fname, &wrap);

    rb_vm_frame_flag_set_box_require(GET_EC());

    VALUE args = rb_ary_new_from_args(2, fname, wrap);
    return rb_load_entrypoint(args);
}
load_path → array Показать исходный код
static VALUE
rb_box_load_path(VALUE box)
{
    VM_ASSERT(BOX_OBJ_P(box));
    return rb_get_box_t(box)->load_path;
}

Возвращает локальный путь загрузки бокса.

require (p1) Показать исходный код
static VALUE
rb_box_require(VALUE box, VALUE fname)
{
    rb_vm_frame_flag_set_box_require(GET_EC());

    return rb_require_string(fname);
}
require_relative (p1) Показать исходный код
static VALUE
rb_box_require_relative(VALUE box, VALUE fname)
{
    rb_vm_frame_flag_set_box_require(GET_EC());

    return rb_require_relative_entrypoint(fname);
}

Ruby Core © 1993–2025 Yukihiro Matsumoto
Licensed under the Ruby License.
Ruby Standard Library © contributors
Licensed under their own licenses.

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API