Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 57 additions & 3 deletions box.c
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ bool ruby_box_crashed = false; // extern, changed only in vm.c

VALUE rb_resolve_feature_path(VALUE klass, VALUE fname);
static VALUE rb_box_inspect(VALUE obj);
static void cleanup_all_local_extensions(VALUE libmap);

void
rb_box_init_done(void)
Expand Down Expand Up @@ -274,6 +275,8 @@ box_entry_free(void *ptr)
st_foreach(box->classext_cow_classes, free_classext_for_box, (st_data_t)box);
}

cleanup_all_local_extensions(box->ruby_dln_libmap);

box_root_free(ptr);
xfree(ptr);
}
Expand Down Expand Up @@ -724,8 +727,57 @@ escaped_basename(const char *path, const char *fname, char *rvalue, size_t rsize
}
}

static void
box_ext_cleanup_mark(void *p)
{
rb_gc_mark((VALUE)p);
}

static void
box_ext_cleanup_free(void *p)
{
VALUE path = (VALUE)p;
unlink(RSTRING_PTR(path));
}

static const rb_data_type_t box_ext_cleanup_type = {
"box_ext_cleanup",
{box_ext_cleanup_mark, box_ext_cleanup_free},
.flags = RUBY_TYPED_FREE_IMMEDIATELY,
};

void
rb_box_cleanup_local_extension(VALUE cleanup)
{
void *p = DATA_PTR(cleanup);
DATA_PTR(cleanup) = NULL;
#ifndef _WIN32
if (p) box_ext_cleanup_free(p);
#endif
}

static int
cleanup_local_extension_i(VALUE key, VALUE value, VALUE arg)
{
#if defined(_WIN32)
HMODULE h = (HMODULE)NUM2SVALUE(value);
WCHAR module_path[MAXPATHLEN];
DWORD len = GetModuleFileNameW(h, module_path, numberof(module_path));

FreeLibrary(h);
if (len > 0 && len < numberof(module_path)) DeleteFileW(module_path);
#endif
return ST_DELETE;
}

static void
cleanup_all_local_extensions(VALUE libmap)
{
rb_hash_foreach(libmap, cleanup_local_extension_i, 0);
}

VALUE
rb_box_local_extension(VALUE box_value, VALUE fname, VALUE path)
rb_box_local_extension(VALUE box_value, VALUE fname, VALUE path, VALUE *cleanup)
{
char ext_path[MAXPATHLEN], fname2[MAXPATHLEN], basename[MAXPATHLEN];
int wrote;
Expand All @@ -739,14 +791,16 @@ rb_box_local_extension(VALUE box_value, VALUE fname, VALUE path)
if (wrote >= (int)sizeof(ext_path)) {
rb_bug("Extension file path in the box was too long");
}
VALUE new_path = rb_str_new_cstr(ext_path);
*cleanup = TypedData_Wrap_Struct(0, &box_ext_cleanup_type, NULL);
enum copy_error_type copy_error = copy_ext_file(src_path, ext_path);
if (copy_error) {
char message[1024];
copy_ext_file_error(message, sizeof(message), copy_error);
rb_raise(rb_eLoadError, "can't prepare the extension file for Ruby Box (%s from %"PRIsVALUE"): %s", ext_path, path, message);
}
// TODO: register the path to be clean-uped
return rb_str_new_cstr(ext_path);
DATA_PTR(*cleanup) = (void *)new_path;
return new_path;
}

// TODO: delete it just after dln_load? or delay it?
Expand Down
21 changes: 17 additions & 4 deletions doc/language/box.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,13 @@ Ruby Box is designed to provide separated spaces in a Ruby process, to isolate a
## Known issues

* Experimental warning is shown when ruby starts with `RUBY_BOX=1` (specify `-W:no-experimental` option to hide it)
* `bundle install` may fail
* `require 'active_support'` may fail
* A wrong current namespace detection happens sometimes in the root namespace
* Installing native extensions may fail under `RUBY_BOX=1` because of stack level too deep in extconf.rb
* `require 'active_support/core_ext'` may fail under `RUBY_BOX=1`
* Defined methods in a box may not be referred by built-in methods written in Ruby

## TODOs

* Add the loaded namespace on iseq to check if another namespace tries running the iseq (add a field only when VM_CHECK_MODE?)
* Delete per-box extension files (.so) lazily or process exit (on Windows)
* Assign its own TOPLEVEL_BINDING in boxes
* Fix calling `warn` in boxes to refer `$VERBOSE` and `Warning.warn` in the box
* Make an internal data container class `Ruby::Box::Entry` invisible
Expand Down Expand Up @@ -258,6 +257,16 @@ Ruby Box works in file scope. One `.rb` file runs in a single box.

Once a file is loaded in a box `box`, all methods/procs defined/created in the file run in `box`.

### Utility methods

Several methods are available for trying/testing Ruby Box.

* `Ruby::Box.current` returns the current box
* `Ruby::Box.enabled?` returns true/false to represent `RUBY_BOX=1` is specified or not
* `Ruby::Box.root` returns the root box
* `Ruby::Box.main` returns the main box
* `Ruby::Box#eval` evaluates a Ruby code (String) in the receiver box, just like calling `#load` with a file

## Implementation details

#### ISeq inline method/constant cache
Expand Down Expand Up @@ -294,6 +303,10 @@ It is a breaking change.

Users can define methods using `Ruby::Box.root.eval(...)`, but it's clearly not ideal API.

#### Assigning values to global variables used by builtin methods

Similar to monkey patching methods, global variables assigned in a box is separated from the root box. Methods defined in the root box referring a global variable can't find the re-assigned one.

#### Context of `$LOAD_PATH` and `$LOADED_FEATURES`

Global variables `$LOAD_PATH` and `$LOADED_FEATURES` control `require` method behaviors. So those variables are determined by the loading box instead of the current box.
Expand Down
8 changes: 4 additions & 4 deletions ext/win32/lib/win32/resolv.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,13 @@ def get_info

if add_search = search.nil?
search = []
nvdom = params.value('NV Domain')
domain = params.value('Domain')

if nvdom and !nvdom.empty?
search = [ nvdom ]
if domain and !domain.empty?
search = [ domain ]
udmnd = params.value('UseDomainNameDevolution')
if udmnd&.nonzero?
if /^\w+\./ =~ nvdom
if /^\w+\./ =~ domain
devo = $'
end
end
Expand Down
1 change: 0 additions & 1 deletion include/ruby/internal/core/rtypeddata.h
Original file line number Diff line number Diff line change
Expand Up @@ -607,7 +607,6 @@ RTYPEDDATA_TYPE(VALUE obj)
return (const struct rb_data_type_struct *)(RTYPEDDATA(obj)->type & TYPED_DATA_PTR_MASK);
}

RBIMPL_ATTR_PURE_UNLESS_DEBUG()
RBIMPL_ATTR_ARTIFICIAL()
/**
* @private
Expand Down
13 changes: 12 additions & 1 deletion internal/box.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@

#include "ruby/ruby.h" /* for VALUE */

#if SIZEOF_VALUE <= SIZEOF_LONG
# define SVALUE2NUM(x) LONG2NUM((long)(x))
# define NUM2SVALUE(x) (SIGNED_VALUE)NUM2LONG(x)
#elif SIZEOF_VALUE <= SIZEOF_LONG_LONG
# define SVALUE2NUM(x) LL2NUM((LONG_LONG)(x))
# define NUM2SVALUE(x) (SIGNED_VALUE)NUM2LL(x)
#else
# error Need integer for VALUE
#endif

/**
* @author Ruby developers <ruby-core@ruby-lang.org>
* @copyright This file is a part of the programming language Ruby.
Expand Down Expand Up @@ -75,7 +85,8 @@ void rb_box_gc_update_references(void *ptr);
rb_box_t * rb_get_box_t(VALUE ns);
VALUE rb_get_box_object(rb_box_t *ns);

VALUE rb_box_local_extension(VALUE box, VALUE fname, VALUE path);
VALUE rb_box_local_extension(VALUE box, VALUE fname, VALUE path, VALUE *cleanup);
void rb_box_cleanup_local_extension(VALUE cleanup);

void rb_initialize_main_box(void);
void rb_box_init_done(void);
Expand Down
16 changes: 5 additions & 11 deletions load.c
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,6 @@
#define IS_SOEXT(e) (strcmp((e), ".so") == 0 || strcmp((e), ".o") == 0)
#define IS_DLEXT(e) (strcmp((e), DLEXT) == 0)

#if SIZEOF_VALUE <= SIZEOF_LONG
# define SVALUE2NUM(x) LONG2NUM((long)(x))
# define NUM2SVALUE(x) (SIGNED_VALUE)NUM2LONG(x)
#elif SIZEOF_VALUE <= SIZEOF_LONG_LONG
# define SVALUE2NUM(x) LL2NUM((LONG_LONG)(x))
# define NUM2SVALUE(x) (SIGNED_VALUE)NUM2LL(x)
#else
# error Need integer for VALUE
#endif

enum {
loadable_ext_rb = (0+ /* .rb extension is the first in both tables */
1) /* offset by rb_find_file_ext() */
Expand Down Expand Up @@ -1203,11 +1193,15 @@ load_ext(VALUE path, VALUE fname)
{
VALUE loaded = path;
const rb_box_t *box = rb_loading_box();
VALUE cleanup = 0;
if (BOX_USER_P(box)) {
loaded = rb_box_local_extension(box->box_object, fname, path);
loaded = rb_box_local_extension(box->box_object, fname, path, &cleanup);
}
rb_scope_visibility_set(METHOD_VISI_PUBLIC);
void *handle = dln_load_feature(RSTRING_PTR(loaded), RSTRING_PTR(fname));
if (cleanup) {
rb_box_cleanup_local_extension(cleanup);
}
RB_GC_GUARD(loaded);
RB_GC_GUARD(fname);
return (VALUE)handle;
Expand Down
16 changes: 12 additions & 4 deletions test/ruby/test_box.rb
Original file line number Diff line number Diff line change
Expand Up @@ -697,10 +697,6 @@ def test_root_and_main_methods
assert !$LOADED_FEATURES.include?("/tmp/barbaz")
assert !Object.const_defined?(:FooClass)
end;
ensure
tmp = ENV["TMPDIR"] || ENV["TMP"] || Etc.systmpdir || "/tmp"
pat = "_ruby_ns_*."+RbConfig::CONFIG["DLEXT"]
File.unlink(*Dir.glob(pat, base: tmp).map {|so| "#{tmp}/#{so}"})
end

def test_basic_box_detections
Expand Down Expand Up @@ -827,4 +823,16 @@ def test_mark_box_object_referred_only_from_binding
assert_equal 42, b.eval('1+2')
end;
end

def test_loaded_extension_deleted_in_user_box
require 'tmpdir'
Dir.mktmpdir do |tmpdir|
env = ENV_ENABLE_BOX.merge({'TMPDIR'=>tmpdir})
assert_ruby_status([env], "#{<<~"begin;"}\n#{<<~'end;'}")
begin;
require "json"
end;
assert_empty(Dir.children(tmpdir))
end
end
end
Loading