winipc 0.1.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +29 -0
- data/LICENSE.txt +21 -0
- data/README.md +154 -0
- data/ext/winipc/extconf.rb +21 -0
- data/ext/winipc/winipc.c +1246 -0
- data/lib/winipc/version.rb +5 -0
- data/lib/winipc.rb +338 -0
- metadata +121 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 628224ea1bed3bec0541f134c74f786132ffc060750d07af59e1f97f0273d484
|
|
4
|
+
data.tar.gz: af11bf3b0527555f5cc1df9542dee9b8e3c8249d52fbf3999e1b461028d052f5
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: a8c03faa73dcddedd6727f5784d311c361ebadd4c93efe16ef1519dbae81e6049bbccc5bdf580606338c55dc7d806dbe1ae026146e7a777547f100329b8da497
|
|
7
|
+
data.tar.gz: 7ec5eb17dc9d44200e40676b0425865f11a79e5e5faf11d33550e0ca8d2a1d3825bbf40773669640e838c11cda362158193b98c7ba8a684e82f7fa4296b72dea
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.0] - 2026-06-01
|
|
4
|
+
|
|
5
|
+
Initial release.
|
|
6
|
+
|
|
7
|
+
- **Named pipes** — `Winipc::Pipe.listen` (server with `#accept`/`#serve`) and
|
|
8
|
+
`Winipc::Pipe.connect` (connect-with-retry client); a duplex `Conn` with
|
|
9
|
+
`#read`/`#write` (byte mode) and `#read_message`/`#write_message` (message
|
|
10
|
+
mode). Overlapped I/O throughout: blocking calls release the GVL and are
|
|
11
|
+
cancelable, and run cooperatively under a fiber scheduler (winloop) by
|
|
12
|
+
offloading to a worker thread. The listener always keeps a pending instance so
|
|
13
|
+
clients never see a between-accepts gap.
|
|
14
|
+
- **Shared memory** — `Winipc::SharedMemory.create`/`.open` over pagefile-backed
|
|
15
|
+
named file mappings, with bounds-checked `#read`/`#write`/`#flush`.
|
|
16
|
+
- **Named synchronization** — `Winipc::Mutex`, `Winipc::Event`, and
|
|
17
|
+
`Winipc::Semaphore` with `create`/`open`, GVL-releasing interruptible waits,
|
|
18
|
+
and auto-releasing `#synchronize` block forms. Abandoned mutexes are surfaced.
|
|
19
|
+
- **Secure by default** — pipe servers restrict access to the current user,
|
|
20
|
+
reject remote clients, and use `FILE_FLAG_FIRST_PIPE_INSTANCE`; handles are
|
|
21
|
+
non-inheritable; objects default to the per-session `Local\` namespace. An
|
|
22
|
+
explicit SDDL string or `:everyone` is opt-in.
|
|
23
|
+
- Error taxonomy under `Winipc::Error` (`OSError` with `#code`, plus
|
|
24
|
+
`TimeoutError`, `NotFound`, `Exists`, `AccessDenied`, `BrokenPipe`, `PipeBusy`,
|
|
25
|
+
`Canceled`, `NotOwner`, `WouldExceedMax`, `ModeError`, `RangeError`, `Closed`,
|
|
26
|
+
`Abandoned`).
|
|
27
|
+
|
|
28
|
+
Pure C extension over kernel32 + advapi32 (`rb_raise`/longjmp-safe; every handle
|
|
29
|
+
freed by a TypedData finalizer). Windows MSVC (mswin) Ruby only.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ned
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# winipc
|
|
2
|
+
|
|
3
|
+
**Windows local IPC for Ruby: named pipes, shared memory, and named synchronization — safe by default, and cooperative under a fiber scheduler.**
|
|
4
|
+
|
|
5
|
+
`winipc` is a native extension that exposes the three pillars of Windows
|
|
6
|
+
inter-process communication through an ergonomic, hard-to-misuse Ruby API:
|
|
7
|
+
|
|
8
|
+
- **Named pipes** — a duplex server and a connect-with-retry client, byte or
|
|
9
|
+
message framed. Opened for overlapped I/O, so blocking calls release the GVL
|
|
10
|
+
and (under a fiber scheduler such as [winloop](https://rubygems.org/gems/winloop))
|
|
11
|
+
run **cooperatively** instead of stalling the loop.
|
|
12
|
+
- **Shared memory** — pagefile-backed named regions (`CreateFileMapping`), with
|
|
13
|
+
bounds-checked reads and writes.
|
|
14
|
+
- **Named synchronization** — cross-process **Mutex**, **Event**, and
|
|
15
|
+
**Semaphore**, with auto-releasing block forms.
|
|
16
|
+
|
|
17
|
+
It is **secure by default**: pipe servers reject remote clients and restrict the
|
|
18
|
+
pipe to the current user (closing the documented default ACL that grants
|
|
19
|
+
`Everyone` read), the first instance is created with
|
|
20
|
+
`FILE_FLAG_FIRST_PIPE_INSTANCE` so a squatter can't pre-own the name, and no
|
|
21
|
+
handle is inheritable.
|
|
22
|
+
|
|
23
|
+
## Requirements
|
|
24
|
+
|
|
25
|
+
- **Windows** with a native **MSVC (mswin)** Ruby. Not supported on MinGW/UCRT.
|
|
26
|
+
- Visual Studio 2017+ / Build Tools with the **Desktop development with C++** workload.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
gem install winipc
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Named pipes
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
require "winipc"
|
|
38
|
+
|
|
39
|
+
# Server (one process)
|
|
40
|
+
Winipc::Pipe.listen("myapp/control") do |server|
|
|
41
|
+
server.serve do |conn| # accept + yield + close, in a loop
|
|
42
|
+
request = conn.read(4096)
|
|
43
|
+
conn.write("ack: #{request}")
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Client (another process)
|
|
48
|
+
Winipc::Pipe.connect("myapp/control", timeout: 5) do |c|
|
|
49
|
+
c.write("hello")
|
|
50
|
+
puts c.read(4096) # => "ack: hello"
|
|
51
|
+
end
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`read` returns `nil` at a clean EOF (peer closed), exactly like `IO#read`; a peer
|
|
55
|
+
dying mid-stream raises `Winipc::BrokenPipe`. **Message mode** preserves message
|
|
56
|
+
boundaries:
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
Winipc::Pipe.listen("svc", mode: :message) { |s| c = s.accept; c.read_message }
|
|
60
|
+
Winipc::Pipe.connect("svc", mode: :message) { |c| c.write_message("one whole message") }
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Cooperative under winloop
|
|
64
|
+
|
|
65
|
+
Because pipe handles are overlapped and the blocking calls release the GVL, a
|
|
66
|
+
server and many clients can run as fibers on a single IOCP loop:
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
require "winloop"
|
|
70
|
+
Winloop.run do
|
|
71
|
+
Fiber.schedule { Winipc::Pipe.listen("svc") { |s| s.serve { |c| c.write(c.read(1024)) } } }
|
|
72
|
+
Fiber.schedule { Winipc::Pipe.connect("svc") { |c| c.write("hi"); c.read(1024) } }
|
|
73
|
+
end
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Under a scheduler, each blocking winipc call is offloaded to a worker thread and
|
|
77
|
+
the calling fiber parks, so the loop keeps serving other fibers. With no
|
|
78
|
+
scheduler, the same calls just block the calling thread (releasing the GVL).
|
|
79
|
+
|
|
80
|
+
## Shared memory
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
# Process A
|
|
84
|
+
shm = Winipc::SharedMemory.create("myapp/buffer", 4096)
|
|
85
|
+
shm.write(0, "shared payload")
|
|
86
|
+
|
|
87
|
+
# Process B
|
|
88
|
+
shm = Winipc::SharedMemory.open("myapp/buffer")
|
|
89
|
+
shm.read(0, 14) # => "shared payload"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Reads and writes are bounds-checked (`Winipc::RangeError` outside the region).
|
|
93
|
+
Pair shared memory with a named `Event` to hand off deterministically rather
|
|
94
|
+
than polling.
|
|
95
|
+
|
|
96
|
+
> **Size note:** the creator's `#size` is exactly the requested size; an
|
|
97
|
+
> **opener**'s `#size` is the page-rounded mapped region (Windows rounds a
|
|
98
|
+
> mapping up to the page boundary and doesn't record the logical size), so an
|
|
99
|
+
> opener may read/write within the rounded region. If the exact length matters,
|
|
100
|
+
> carry it out-of-band (e.g. store it in the first bytes of the region).
|
|
101
|
+
|
|
102
|
+
## Named synchronization
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
# Cross-process mutex
|
|
106
|
+
m = Winipc::Mutex.create("myapp/lock")
|
|
107
|
+
m.synchronize { critical_section } # always released, even on exception
|
|
108
|
+
|
|
109
|
+
# Event (one process waits, another signals)
|
|
110
|
+
ev = Winipc::Event.create("myapp/ready", manual_reset: true)
|
|
111
|
+
ev.wait(timeout: 10) # => true (signaled) / false (timed out)
|
|
112
|
+
ev.signal
|
|
113
|
+
|
|
114
|
+
# Counting semaphore
|
|
115
|
+
sem = Winipc::Semaphore.create("myapp/slots", initial: 3, maximum: 3)
|
|
116
|
+
sem.synchronize { use_a_slot }
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`Mutex` is owned by the acquiring **thread**, so its waits are not offloaded to a
|
|
120
|
+
worker (they release the GVL but, under a fiber scheduler, block the loop for the
|
|
121
|
+
duration). Use `Event`/`Semaphore` — which any thread may wait on and signal —
|
|
122
|
+
for fiber-cooperative coordination. An abandoned mutex (its holder died) is
|
|
123
|
+
surfaced as `Winipc::Abandoned` / `#abandoned?` rather than silently ignored.
|
|
124
|
+
|
|
125
|
+
## Errors
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
StandardError
|
|
129
|
+
└─ Winipc::Error
|
|
130
|
+
├─ Winipc::OSError # a Windows API failed; #code is GetLastError
|
|
131
|
+
│ ├─ Winipc::TimeoutError ├─ Winipc::BrokenPipe ├─ Winipc::NotOwner
|
|
132
|
+
│ ├─ Winipc::NotFound ├─ Winipc::PipeBusy └─ Winipc::WouldExceedMax
|
|
133
|
+
│ ├─ Winipc::Exists ├─ Winipc::Canceled
|
|
134
|
+
│ └─ Winipc::AccessDenied
|
|
135
|
+
├─ Winipc::ModeError # byte/message-mode misuse
|
|
136
|
+
├─ Winipc::RangeError # shared-memory access out of bounds
|
|
137
|
+
├─ Winipc::Closed # operating on a closed object
|
|
138
|
+
└─ Winipc::Abandoned # a mutex's previous owner died holding it
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Notes
|
|
142
|
+
|
|
143
|
+
- **Names** are bare (`"myapp/control"`); winipc maps them to `\\.\pipe\…` for
|
|
144
|
+
pipes and the `Local\` kernel namespace for shared memory / sync objects (pass
|
|
145
|
+
`scope: :global` for the `Global\` namespace, which may need privileges).
|
|
146
|
+
- Every kernel handle lives in a wrapper whose finalizer closes it, so a
|
|
147
|
+
forgotten `#close` never leaks — but closing explicitly is good manners.
|
|
148
|
+
- Blocking calls (`accept`, `read`, `write`, `wait`/`lock`/`acquire`) release the
|
|
149
|
+
GVL and are interruptible (`Thread#kill`, `Ctrl-C`, `Timeout`).
|
|
150
|
+
- **Windows/MSVC only.** Links `kernel32` + `advapi32`, built with `cl.exe`.
|
|
151
|
+
|
|
152
|
+
## License
|
|
153
|
+
|
|
154
|
+
[MIT](LICENSE.txt).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
#
|
|
3
|
+
# extconf.rb for the winipc extension — Windows local IPC (named pipes,
|
|
4
|
+
# shared memory / file mappings, and named synchronization objects).
|
|
5
|
+
|
|
6
|
+
require "mkmf"
|
|
7
|
+
|
|
8
|
+
unless RbConfig::CONFIG["target_os"] =~ /mswin/
|
|
9
|
+
abort <<~MSG
|
|
10
|
+
winipc requires a native Windows MSVC (mswin) Ruby — it binds Win32 named
|
|
11
|
+
pipes, file-mapping shared memory, and named synchronization objects, and is
|
|
12
|
+
built with cl.exe. Your Ruby is "#{RbConfig::CONFIG['arch']}".
|
|
13
|
+
MSG
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# kernel32 (pipes / file mappings / sync objects) is linked by default.
|
|
17
|
+
# advapi32 provides ConvertStringSecurityDescriptorToSecurityDescriptorW for the
|
|
18
|
+
# safe-by-default security descriptors. Pure C — no -EHsc.
|
|
19
|
+
$libs = [$libs, "advapi32.lib"].join(" ")
|
|
20
|
+
|
|
21
|
+
create_makefile("winipc/winipc")
|