cruise 0.0.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/Cargo.toml ADDED
@@ -0,0 +1,3 @@
1
+ [workspace]
2
+ members = ["ext/cruise"]
3
+ resolver = "2"
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Marco Roth
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,223 @@
1
+ <div align="center">
2
+ <h1>Cruise</h1>
3
+ <h4>A fast, OS-native file watcher for Ruby.</h4>
4
+
5
+ <p>
6
+ <a href="https://rubygems.org/gems/cruise"><img alt="Gem Version" src="https://img.shields.io/gem/v/cruise"></a>
7
+ <a href="https://github.com/marcoroth/cruise/blob/main/LICENSE.txt"><img alt="License" src="https://img.shields.io/github/license/marcoroth/cruise"></a>
8
+ </p>
9
+
10
+ <p>A file system watcher built on native OS events, not polling.<br/>Uses FSEvents on macOS and inotify on Linux.</p>
11
+ </div>
12
+
13
+ Cruise was originally built to power file watching in the [Herb](https://herb-tools.dev) dev server, none of the existing Ruby watchers were quite reliable enough for the realtime hot reloading that a local development server and workflow needs. It also works standalone for any project that needs to react to file changes.
14
+
15
+ ## Installation
16
+
17
+ **Add to your Gemfile:**
18
+
19
+ ```ruby
20
+ gem "cruise"
21
+ ```
22
+
23
+ **Or install directly:**
24
+
25
+ ```bash
26
+ gem install cruise
27
+ ```
28
+
29
+ Precompiled native gems are available for macOS and Linux. If a precompiled gem isn't available for your platform, it will compile from source (requires Rust).
30
+
31
+ ## Usage
32
+
33
+ ### Basic Example
34
+
35
+ Watch a directory for changes:
36
+
37
+ ```ruby
38
+ require "cruise"
39
+
40
+ Cruise.watch("app/views") do |event|
41
+ puts "#{event.kind}: #{event.path}"
42
+ end
43
+ ```
44
+
45
+ The callback receives a `Cruise::Event` with two attributes:
46
+
47
+ | Attribute | Description |
48
+ |--------------|----------------------------------------------------------------------------------------|
49
+ | `event.path` | Absolute path to the changed file |
50
+ | `event.kind` | One of: `"created"`, `"modified"`, `"renamed"`, `"removed"`, `"accessed"`, `"changed"` |
51
+
52
+ ### Multiple Directories
53
+
54
+ Watch several paths at once:
55
+
56
+ ```ruby
57
+ Cruise.watch("app/views", "app/components", "lib/templates") do |event|
58
+ puts event.inspect
59
+ # => #<Cruise::Event kind="modified" path="/app/views/users/show.html.erb">
60
+ end
61
+ ```
62
+
63
+ ### Ruby Threads
64
+
65
+ Cruise releases the GVL while waiting for filesystem events, so Ruby threads run freely:
66
+
67
+ ```ruby
68
+ Thread.new { do_background_work }
69
+
70
+ Cruise.watch("src") do |event|
71
+ puts event
72
+ end
73
+ ```
74
+
75
+ ### Glob Filtering
76
+
77
+ Only receive events for files matching a pattern:
78
+
79
+ ```ruby
80
+ Cruise.watch("app/views", glob: "**/*.html.erb") do |event|
81
+ puts event
82
+ end
83
+ ```
84
+
85
+ Multiple patterns:
86
+
87
+ ```ruby
88
+ Cruise.watch("app", glob: ["**/*.html.erb", "**/*.html"]) do |event|
89
+ puts event
90
+ end
91
+ ```
92
+
93
+ ### Debounce
94
+
95
+ Configure the debounce interval (default: 100ms):
96
+
97
+ ```ruby
98
+ Cruise.watch("src", debounce: 0.5) do |event|
99
+ puts event
100
+ end
101
+ ```
102
+
103
+ ### Proc Callback
104
+
105
+ You can also pass a `Proc` via the `callback:` keyword:
106
+
107
+ ```ruby
108
+ handler = proc { |event| puts event }
109
+
110
+ Cruise.watch("app/views", callback: handler)
111
+ ```
112
+
113
+ ### Stopping
114
+
115
+ The watcher runs in a blocking loop. Use `Interrupt` to stop it cleanly:
116
+
117
+ ```ruby
118
+ begin
119
+ Cruise.watch("app") do |event|
120
+ # process event
121
+ end
122
+ rescue Interrupt
123
+ puts "Stopped."
124
+ end
125
+ ```
126
+
127
+ ### Async / Fibers
128
+
129
+ `Cruise.watch` cooperates with Ruby's fiber scheduler, the API is identical whether you're on threads or fibers, with no separate "async mode." It waits on an internal pipe via `IO#wait_readable`, which yields to other fibers when a scheduler is active (for example inside [`Async`](https://github.com/socketry/async)) and otherwise blocks the thread with the GVL released.
130
+
131
+ ```ruby
132
+ require "async"
133
+ require "cruise"
134
+
135
+ Async do |task|
136
+ task.async do
137
+ Cruise.watch("app/views", glob: "**/*.html.erb") do |event|
138
+ reload(event.path)
139
+ end
140
+ end
141
+
142
+ task.async do
143
+ start_dev_server
144
+ end
145
+ end
146
+ ```
147
+
148
+ Cruise doesn't depend on `async`, it only relies on the standard `IO#wait_readable` hook, so any `Fiber::Scheduler` works.
149
+
150
+ #### Lower-level watcher
151
+
152
+ For custom event loops or manually-driven fibers, `Cruise::Watcher` exposes a readable `IO` and a non-blocking `#poll`:
153
+
154
+ ```ruby
155
+ watcher = Cruise::Watcher.new("app/views", glob: "**/*.erb", debounce: 0.1)
156
+
157
+ begin
158
+ loop do
159
+ watcher.io.wait_readable
160
+ watcher.io.read_nonblock(4096, exception: false)
161
+
162
+ while (event = watcher.poll)
163
+ handle(event)
164
+ end
165
+ end
166
+ ensure
167
+ watcher.close
168
+ end
169
+ ```
170
+
171
+ `Cruise::Watcher.new` takes the same paths keyword arguments as `Cruise.watch`, it's the watcher `Cruise.watch` runs internally.
172
+
173
+ ## How It Works
174
+
175
+ Cruise is a Ruby C extension that links a Rust static library (`libcruise.a`) built around the [notify](https://github.com/notify-rs/notify) crate. The Rust core exposes a small C ABI (generated with [cbindgen](https://github.com/mozilla/cbindgen)) and knows nothing about Ruby. A thin hand-written C wrapper bridges that ABI to Ruby objects via the Ruby C API.
176
+
177
+ 1. `Cruise.watch` sets up a [notify](https://github.com/notify-rs/notify) watcher with event debouncing (100ms)
178
+ 2. A background thread monitors filesystem events using the OS-native API
179
+ 3. The main loop calls `rb_thread_call_without_gvl` to wait without blocking Ruby
180
+ 4. When an event arrives, the GVL is re-acquired and your callback is invoked
181
+
182
+ ### Platform Backends
183
+
184
+ | Platform | Backend | API |
185
+ |----------|----------|--------------------------|
186
+ | macOS | FSEvents | `CoreServices` framework |
187
+ | Linux | inotify | `inotify_init1` syscall |
188
+
189
+ All backends watch recursively by default.
190
+
191
+ ## Prior Art
192
+
193
+ Cruise builds on ideas from the Ruby ecosystem's existing file watchers:
194
+
195
+ - **[`listen`](https://github.com/guard/listen)** the long-standing workhorse behind Guard, Rails, and much of the ecosystem. Wraps [rb-fsevent](https://github.com/guard/rb-fsevent) (macOS) and [rb-inotify](https://github.com/guard/rb-inotify) (Linux), with a polling fallback for other platforms and network shares.
196
+ - **[`guard`](https://github.com/guard/guard)** a command-line tool, built on listen, that runs tasks (tests, reloads, builds) in response to file changes.
197
+ - **[`io-watch`](https://github.com/socketry/io-watch)** a newer, deliberately minimal library from the [socketry](https://github.com/socketry) project, offering one simple, unified directory-watching interface across platforms without per-platform multiplexing in application code.
198
+
199
+ Cruise shares `io-watch`'s preference for a single, native-events-only interface over platform multiplexing. On top of that it focuses on what a hot-reload loop needs: built-in debouncing, glob and event-kind filtering, and precompiled native gems so there's nothing to build at install time on supported platforms. It deliberately has no polling adapter, so filesystems where native events aren't delivered fall outside its scope, reach for [listen](https://github.com/guard/listen) there.
200
+
201
+ ## Development
202
+
203
+ **Requirements:** Rust toolchain, Ruby 3.2+
204
+
205
+ ```bash
206
+ git clone https://github.com/marcoroth/cruise
207
+ cd cruise
208
+ bundle install
209
+ bundle exec rake compile
210
+ bundle exec rake test
211
+ ```
212
+
213
+ ### Cross-compilation
214
+
215
+ Cruise uses [rake-compiler](https://github.com/rake-compiler/rake-compiler) and [rake-compiler-dock](https://github.com/rake-compiler/rake-compiler-dock) for building native gems:
216
+
217
+ ```bash
218
+ bundle exec rake gem:native
219
+ ```
220
+
221
+ ## License
222
+
223
+ MIT License. See [LICENSE.txt](LICENSE.txt).
data/Rakefile CHANGED
@@ -1,12 +1,89 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bundler/gem_tasks"
4
- require "minitest/test_task"
4
+ require "rake/testtask"
5
5
 
6
- Minitest::TestTask.create
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.libs << "ext"
10
+ t.test_files = FileList["test/**/*_test.rb"]
11
+ end
7
12
 
8
- require "rubocop/rake_task"
13
+ begin
14
+ require "rake/extensiontask"
9
15
 
10
- RuboCop::RakeTask.new
16
+ PLATFORMS = [
17
+ "aarch64-linux-gnu",
18
+ "aarch64-linux-musl",
19
+ "arm-linux-gnu",
20
+ "arm64-darwin",
21
+ "x86_64-darwin",
22
+ "x86_64-linux-gnu",
23
+ "x86_64-linux-musl"
24
+ ].freeze
11
25
 
12
- task default: %i[test rubocop]
26
+ RB_SYS_PLATFORM_MAP = {
27
+ "aarch64-linux-gnu" => "aarch64-linux",
28
+ "aarch64-linux-musl" => "aarch64-linux-musl",
29
+ "arm-linux-gnu" => "arm-linux",
30
+ "arm64-darwin" => "arm64-darwin",
31
+ "x86_64-darwin" => "x86_64-darwin",
32
+ "x86_64-linux-gnu" => "x86_64-linux",
33
+ "x86_64-linux-musl" => "x86_64-linux-musl",
34
+ }.freeze
35
+
36
+ Rake::ExtensionTask.new do |ext|
37
+ ext.name = "cruise"
38
+ ext.source_pattern = "*.{c,h}"
39
+ ext.ext_dir = "ext/cruise"
40
+ ext.lib_dir = "lib/cruise"
41
+ ext.gem_spec = Gem::Specification.load("cruise.gemspec")
42
+ ext.cross_compile = true
43
+ ext.cross_platform = PLATFORMS
44
+ end
45
+
46
+ namespace "gem" do
47
+ task "prepare" do
48
+ require "rake_compiler_dock"
49
+
50
+ sh "bundle config set cache_all true"
51
+
52
+ gemspec_path = File.expand_path("./cruise.gemspec", __dir__)
53
+ spec = eval(File.read(gemspec_path), binding, gemspec_path)
54
+
55
+ RakeCompilerDock.set_ruby_cc_version(spec.required_ruby_version.as_list)
56
+ rescue LoadError
57
+ abort "rake_compiler_dock is required for this task"
58
+ end
59
+
60
+ PLATFORMS.each do |platform|
61
+ desc "Build all native binary gems in parallel"
62
+ multitask "native" => platform
63
+
64
+ desc "Build the native gem for #{platform}"
65
+ task platform => "prepare" do
66
+ require "rb_sys"
67
+
68
+ rb_sys_platform = RB_SYS_PLATFORM_MAP.fetch(platform)
69
+
70
+ RakeCompilerDock.sh(
71
+ "export RCD_PLATFORM=#{platform} && " \
72
+ "bundle --local && rake native:#{platform} gem RUBY_CC_VERSION='#{ENV.fetch("RUBY_CC_VERSION", nil)}'",
73
+ platform: platform,
74
+ image: "rbsys/#{rb_sys_platform}:#{RbSys::VERSION}"
75
+ )
76
+ end
77
+ end
78
+ end
79
+ rescue LoadError => e
80
+ warn "WARNING: Failed to load extension tasks: #{e.message}"
81
+
82
+ desc "Compile task not available (rake-compiler not installed)"
83
+ task :compile do
84
+ abort "rake-compiler is required: #{e.message}\n\nRun: bundle install"
85
+ end
86
+ end
87
+
88
+ task test: :compile
89
+ task default: [:compile, :test]
data/cruise.gemspec CHANGED
@@ -8,21 +8,33 @@ Gem::Specification.new do |spec|
8
8
  spec.authors = ["Marco Roth"]
9
9
  spec.email = ["marco.roth@intergga.ch"]
10
10
 
11
- spec.summary = "A fast, native file watcher for Ruby"
12
- spec.description = "Cruise is a Rust-powered file system watcher with native OS integration. Uses FSEvents on macOS and inotify on Linux."
11
+ spec.summary = "A fast, OS-native file watcher for Ruby"
12
+ spec.description = "Cruise is a file system watcher built on native OS events. Uses FSEvents on macOS and inotify on Linux."
13
13
  spec.homepage = "https://github.com/marcoroth/cruise"
14
14
  spec.license = "MIT"
15
15
 
16
- spec.required_ruby_version = ">= 3.1.0"
16
+ spec.required_ruby_version = ">= 3.2.0"
17
17
  spec.require_paths = ["lib"]
18
18
 
19
19
  spec.files = Dir[
20
20
  "cruise.gemspec",
21
21
  "LICENSE.txt",
22
+ "README.md",
23
+ "Cargo.toml",
24
+ "Cargo.lock",
22
25
  "Rakefile",
23
- "lib/**/*.rb"
26
+ "lib/**/*.rb",
27
+ "ext/cruise/build.rs",
28
+ "ext/cruise/Cargo.toml",
29
+ "ext/cruise/cbindgen.toml",
30
+ "ext/cruise/cruise.c",
31
+ "ext/cruise/extconf.rb",
32
+ "ext/cruise/include/**/*.h",
33
+ "ext/cruise/src/**/*.rs"
24
34
  ]
25
35
 
36
+ spec.extensions = ["ext/cruise/extconf.rb"]
37
+
26
38
  spec.metadata["allowed_push_host"] = "https://rubygems.org"
27
39
  spec.metadata["rubygems_mfa_required"] = "true"
28
40
  spec.metadata["homepage_uri"] = spec.homepage
@@ -0,0 +1,22 @@
1
+ [package]
2
+ name = "cruise"
3
+ version = "0.2.0"
4
+ edition = "2021"
5
+ authors = ["Marco Roth <marco.roth@intergga.ch>"]
6
+ description = "A fast, OS-native file watcher for Ruby"
7
+ license = "MIT"
8
+ publish = false
9
+
10
+ [lib]
11
+ name = "cruise"
12
+ path = "src/lib.rs"
13
+ crate-type = ["staticlib"]
14
+
15
+ [dependencies]
16
+ notify = { version = "7", features = ["macos_fsevent"] }
17
+ notify-debouncer-full = "0.4"
18
+ globset = "0.4"
19
+ libc = "0.2"
20
+
21
+ [build-dependencies]
22
+ cbindgen = "0.28"
@@ -0,0 +1,15 @@
1
+ use std::env;
2
+ use std::fs;
3
+ use std::path::PathBuf;
4
+
5
+ fn main() {
6
+ let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
7
+ let include_dir = PathBuf::from(&crate_dir).join("include");
8
+ let header_path = include_dir.join("cruise.h");
9
+
10
+ let _ = fs::create_dir_all(&include_dir);
11
+
12
+ if let Ok(bindings) = cbindgen::generate(&crate_dir) {
13
+ bindings.write_to_file(&header_path);
14
+ }
15
+ }
@@ -0,0 +1,23 @@
1
+ language = "C"
2
+ header = """/* Generated by cbindgen, do not edit manually */
3
+
4
+ #include <stdbool.h>
5
+ #include <stdint.h>
6
+ #include <stddef.h>"""
7
+ include_guard = "CRUISE_H"
8
+ no_includes = true
9
+
10
+ [export]
11
+ include = [
12
+ "CruiseEvent",
13
+ ]
14
+
15
+ [enum]
16
+ rename_variants = "ScreamingSnakeCase"
17
+ prefix_with_name = true
18
+
19
+ [fn]
20
+ sort_by = "None"
21
+
22
+ [parse]
23
+ parse_deps = false
@@ -0,0 +1,158 @@
1
+ #include <ruby.h>
2
+ #include <ruby/encoding.h>
3
+ #include <string.h>
4
+ #include "include/cruise.h"
5
+
6
+ static VALUE rb_mCruise;
7
+ static VALUE rb_cWatcher;
8
+ static VALUE rb_cEvent;
9
+
10
+ static VALUE make_utf8_string(const char *cstring) {
11
+ return rb_enc_str_new_cstr(cstring, rb_utf8_encoding());
12
+ }
13
+
14
+ static void fill_c_strings(VALUE array, const char **buffer) {
15
+ long length = RARRAY_LEN(array);
16
+
17
+ for (long index = 0; index < length; index++) {
18
+ VALUE string = rb_ary_entry(array, index);
19
+
20
+ buffer[index] = StringValueCStr(string);
21
+ }
22
+ }
23
+
24
+ static void watcher_free(void *pointer) {
25
+ if (pointer) {
26
+ cruise_watcher_free((struct Watcher *) pointer);
27
+ }
28
+ }
29
+
30
+ static size_t watcher_size(const void *pointer) {
31
+ (void) pointer;
32
+
33
+ return sizeof(void *);
34
+ }
35
+
36
+ static const rb_data_type_t watcher_type = {
37
+ .wrap_struct_name = "Cruise::Watcher",
38
+ .function = {
39
+ .dfree = watcher_free,
40
+ .dsize = watcher_size,
41
+ },
42
+ .flags = RUBY_TYPED_FREE_IMMEDIATELY,
43
+ };
44
+
45
+ static VALUE watcher_alloc(VALUE klass) {
46
+ return TypedData_Wrap_Struct(klass, &watcher_type, NULL);
47
+ }
48
+
49
+ static struct Watcher *get_watcher(VALUE self) {
50
+ struct Watcher *watcher;
51
+
52
+ TypedData_Get_Struct(self, struct Watcher, &watcher_type, watcher);
53
+
54
+ return watcher;
55
+ }
56
+
57
+ static VALUE watcher_initialize_native(VALUE self, VALUE paths, VALUE debounce, VALUE globs, VALUE only_kinds) {
58
+ long path_count = RARRAY_LEN(paths);
59
+ long glob_count = RARRAY_LEN(globs);
60
+ long only_count = RARRAY_LEN(only_kinds);
61
+
62
+ const char **path_pointers = path_count > 0 ? ALLOCA_N(const char *, path_count) : NULL;
63
+ const char **glob_pointers = glob_count > 0 ? ALLOCA_N(const char *, glob_count) : NULL;
64
+ const char **only_pointers = only_count > 0 ? ALLOCA_N(const char *, only_count) : NULL;
65
+
66
+ if (path_pointers) fill_c_strings(paths, path_pointers);
67
+ if (glob_pointers) fill_c_strings(globs, glob_pointers);
68
+ if (only_pointers) fill_c_strings(only_kinds, only_pointers);
69
+
70
+ int read_fd = -1;
71
+ char *error = NULL;
72
+
73
+ struct Watcher *watcher = cruise_watcher_new(
74
+ path_pointers, (size_t) path_count,
75
+ NUM2DBL(debounce),
76
+ glob_pointers, (size_t) glob_count,
77
+ only_pointers, (size_t) only_count,
78
+ &read_fd,
79
+ &error
80
+ );
81
+
82
+ if (!watcher) {
83
+ VALUE message = error ? make_utf8_string(error) : rb_str_new_cstr("Failed to create watcher");
84
+
85
+ if (error) cruise_string_free(error);
86
+
87
+ const char *message_cstr = StringValueCStr(message);
88
+ VALUE error_class = rb_eRuntimeError;
89
+
90
+ if (strstr(message_cstr, "does not exist") || strstr(message_cstr, "Invalid glob pattern")) {
91
+ error_class = rb_eArgError;
92
+ }
93
+
94
+ rb_raise(error_class, "%s", message_cstr);
95
+ }
96
+
97
+ RTYPEDDATA_DATA(self) = watcher;
98
+
99
+ VALUE io = rb_funcall(rb_cIO, rb_intern("for_fd"), 2, INT2NUM(read_fd), rb_str_new_cstr("r"));
100
+ rb_iv_set(self, "@io", io);
101
+
102
+ return self;
103
+ }
104
+
105
+ // watcher.io
106
+ static VALUE watcher_io(VALUE self) {
107
+ return rb_iv_get(self, "@io");
108
+ }
109
+
110
+ // watcher.poll
111
+ static VALUE watcher_poll(VALUE self) {
112
+ struct Watcher *watcher = get_watcher(self);
113
+
114
+ if (!watcher) return Qnil;
115
+
116
+ CruiseEvent event = { NULL, NULL };
117
+
118
+ if (!cruise_watcher_poll(watcher, &event)) return Qnil;
119
+
120
+ VALUE path = make_utf8_string(event.path);
121
+ VALUE kind = make_utf8_string(event.kind);
122
+
123
+ cruise_string_free(event.path);
124
+ cruise_string_free(event.kind);
125
+
126
+ return rb_funcall(rb_cEvent, rb_intern("new"), 2, path, kind);
127
+ }
128
+
129
+ // watcher.close
130
+ static VALUE watcher_close(VALUE self) {
131
+ struct Watcher *watcher = get_watcher(self);
132
+
133
+ if (watcher) {
134
+ cruise_watcher_free(watcher);
135
+ RTYPEDDATA_DATA(self) = NULL;
136
+ }
137
+
138
+ VALUE io = rb_iv_get(self, "@io");
139
+
140
+ if (!NIL_P(io) && !RTEST(rb_funcall(io, rb_intern("closed?"), 0))) {
141
+ rb_funcall(io, rb_intern("close"), 0);
142
+ }
143
+
144
+ return Qnil;
145
+ }
146
+
147
+ void Init_cruise(void) {
148
+ rb_mCruise = rb_define_module("Cruise");
149
+ rb_cEvent = rb_const_get(rb_mCruise, rb_intern("Event"));
150
+
151
+ rb_cWatcher = rb_define_class_under(rb_mCruise, "Watcher", rb_cObject);
152
+
153
+ rb_define_alloc_func(rb_cWatcher, watcher_alloc);
154
+ rb_define_private_method(rb_cWatcher, "initialize_native", watcher_initialize_native, 4);
155
+ rb_define_method(rb_cWatcher, "io", watcher_io, 0);
156
+ rb_define_method(rb_cWatcher, "poll", watcher_poll, 0);
157
+ rb_define_method(rb_cWatcher, "close", watcher_close, 0);
158
+ }