tokra 0.0.1.pre.1
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/.pre-commit-config.yaml +16 -0
- data/AGENTS.md +126 -0
- data/CHANGELOG.md +21 -0
- data/CODE_OF_CONDUCT.md +16 -0
- data/Cargo.toml +23 -0
- data/LICENSE +661 -0
- data/LICENSES/AGPL-3.0-or-later.txt +235 -0
- data/LICENSES/Apache-2.0.txt +73 -0
- data/LICENSES/CC-BY-SA-4.0.txt +170 -0
- data/LICENSES/CC0-1.0.txt +121 -0
- data/LICENSES/MIT.txt +18 -0
- data/README.md +45 -0
- data/README.rdoc +4 -0
- data/REUSE.toml +11 -0
- data/Rakefile +27 -0
- data/Steepfile +15 -0
- data/clippy.toml +5 -0
- data/clippy_exceptions.rb +59 -0
- data/doc/contributors/adr/001.md +187 -0
- data/doc/contributors/adr/002.md +132 -0
- data/doc/contributors/adr/003.md +116 -0
- data/doc/contributors/chats/001.md +3874 -0
- data/doc/contributors/plan/001.md +271 -0
- data/examples/verify_hello_world/app.rb +114 -0
- data/examples/verify_hello_world/index.html +88 -0
- data/examples/verify_ping_pong/README.md +0 -0
- data/examples/verify_ping_pong/app.rb +132 -0
- data/examples/verify_ping_pong/public/styles.css +182 -0
- data/examples/verify_ping_pong/views/index.erb +94 -0
- data/examples/verify_ping_pong/views/layout.erb +22 -0
- data/exe/semantic-highlight +0 -0
- data/ext/tokra/Cargo.toml +23 -0
- data/ext/tokra/extconf.rb +12 -0
- data/ext/tokra/src/lib.rs +719 -0
- data/lib/tokra/native.rb +79 -0
- data/lib/tokra/rack/handler.rb +177 -0
- data/lib/tokra/version.rb +12 -0
- data/lib/tokra.rb +19 -0
- data/mise.toml +8 -0
- data/rustfmt.toml +4 -0
- data/sig/tokra.rbs +7 -0
- data/tasks/lint.rake +151 -0
- data/tasks/rust.rake +63 -0
- data/tasks/steep.rake +11 -0
- data/tasks/test.rake +26 -0
- data/test_native.rb +37 -0
- data/vendor/goodcop/base.yml +1047 -0
- metadata +112 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
Creative Commons Legal Code
|
|
2
|
+
|
|
3
|
+
CC0 1.0 Universal
|
|
4
|
+
|
|
5
|
+
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
|
|
6
|
+
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
|
|
7
|
+
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
|
|
8
|
+
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
|
|
9
|
+
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
|
|
10
|
+
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
|
|
11
|
+
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
|
|
12
|
+
HEREUNDER.
|
|
13
|
+
|
|
14
|
+
Statement of Purpose
|
|
15
|
+
|
|
16
|
+
The laws of most jurisdictions throughout the world automatically confer
|
|
17
|
+
exclusive Copyright and Related Rights (defined below) upon the creator
|
|
18
|
+
and subsequent owner(s) (each and all, an "owner") of an original work of
|
|
19
|
+
authorship and/or a database (each, a "Work").
|
|
20
|
+
|
|
21
|
+
Certain owners wish to permanently relinquish those rights to a Work for
|
|
22
|
+
the purpose of contributing to a commons of creative, cultural and
|
|
23
|
+
scientific works ("Commons") that the public can reliably and without fear
|
|
24
|
+
of later claims of infringement build upon, modify, incorporate in other
|
|
25
|
+
works, reuse and redistribute as freely as possible in any form whatsoever
|
|
26
|
+
and for any purposes, including without limitation commercial purposes.
|
|
27
|
+
These owners may contribute to the Commons to promote the ideal of a free
|
|
28
|
+
culture and the further production of creative, cultural and scientific
|
|
29
|
+
works, or to gain reputation or greater distribution for their Work in
|
|
30
|
+
part through the use and efforts of others.
|
|
31
|
+
|
|
32
|
+
For these and/or other purposes and motivations, and without any
|
|
33
|
+
expectation of additional consideration or compensation, the person
|
|
34
|
+
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
|
|
35
|
+
is an owner of Copyright and Related Rights in the Work, voluntarily
|
|
36
|
+
elects to apply CC0 to the Work and publicly distribute the Work under its
|
|
37
|
+
terms, with knowledge of his or her Copyright and Related Rights in the
|
|
38
|
+
Work and the meaning and intended legal effect of CC0 on those rights.
|
|
39
|
+
|
|
40
|
+
1. Copyright and Related Rights. A Work made available under CC0 may be
|
|
41
|
+
protected by copyright and related or neighboring rights ("Copyright and
|
|
42
|
+
Related Rights"). Copyright and Related Rights include, but are not
|
|
43
|
+
limited to, the following:
|
|
44
|
+
|
|
45
|
+
i. the right to reproduce, adapt, distribute, perform, display,
|
|
46
|
+
communicate, and translate a Work;
|
|
47
|
+
ii. moral rights retained by the original author(s) and/or performer(s);
|
|
48
|
+
iii. publicity and privacy rights pertaining to a person's image or
|
|
49
|
+
likeness depicted in a Work;
|
|
50
|
+
iv. rights protecting against unfair competition in regards to a Work,
|
|
51
|
+
subject to the limitations in paragraph 4(a), below;
|
|
52
|
+
v. rights protecting the extraction, dissemination, use and reuse of data
|
|
53
|
+
in a Work;
|
|
54
|
+
vi. database rights (such as those arising under Directive 96/9/EC of the
|
|
55
|
+
European Parliament and of the Council of 11 March 1996 on the legal
|
|
56
|
+
protection of databases, and under any national implementation
|
|
57
|
+
thereof, including any amended or successor version of such
|
|
58
|
+
directive); and
|
|
59
|
+
vii. other similar, equivalent or corresponding rights throughout the
|
|
60
|
+
world based on applicable law or treaty, and any national
|
|
61
|
+
implementations thereof.
|
|
62
|
+
|
|
63
|
+
2. Waiver. To the greatest extent permitted by, but not in contravention
|
|
64
|
+
of, applicable law, Affirmer hereby overtly, fully, permanently,
|
|
65
|
+
irrevocably and unconditionally waives, abandons, and surrenders all of
|
|
66
|
+
Affirmer's Copyright and Related Rights and associated claims and causes
|
|
67
|
+
of action, whether now known or unknown (including existing as well as
|
|
68
|
+
future claims and causes of action), in the Work (i) in all territories
|
|
69
|
+
worldwide, (ii) for the maximum duration provided by applicable law or
|
|
70
|
+
treaty (including future time extensions), (iii) in any current or future
|
|
71
|
+
medium and for any number of copies, and (iv) for any purpose whatsoever,
|
|
72
|
+
including without limitation commercial, advertising or promotional
|
|
73
|
+
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
|
|
74
|
+
member of the public at large and to the detriment of Affirmer's heirs and
|
|
75
|
+
successors, fully intending that such Waiver shall not be subject to
|
|
76
|
+
revocation, rescission, cancellation, termination, or any other legal or
|
|
77
|
+
equitable action to disrupt the quiet enjoyment of the Work by the public
|
|
78
|
+
as contemplated by Affirmer's express Statement of Purpose.
|
|
79
|
+
|
|
80
|
+
3. Public License Fallback. Should any part of the Waiver for any reason
|
|
81
|
+
be judged legally invalid or ineffective under applicable law, then the
|
|
82
|
+
Waiver shall be preserved to the maximum extent permitted taking into
|
|
83
|
+
account Affirmer's express Statement of Purpose. In addition, to the
|
|
84
|
+
extent the Waiver is so judged Affirmer hereby grants to each affected
|
|
85
|
+
person a royalty-free, non transferable, non sublicensable, non exclusive,
|
|
86
|
+
irrevocable and unconditional license to exercise Affirmer's Copyright and
|
|
87
|
+
Related Rights in the Work (i) in all territories worldwide, (ii) for the
|
|
88
|
+
maximum duration provided by applicable law or treaty (including future
|
|
89
|
+
time extensions), (iii) in any current or future medium and for any number
|
|
90
|
+
of copies, and (iv) for any purpose whatsoever, including without
|
|
91
|
+
limitation commercial, advertising or promotional purposes (the
|
|
92
|
+
"License"). The License shall be deemed effective as of the date CC0 was
|
|
93
|
+
applied by Affirmer to the Work. Should any part of the License for any
|
|
94
|
+
reason be judged legally invalid or ineffective under applicable law, such
|
|
95
|
+
partial invalidity or ineffectiveness shall not invalidate the remainder
|
|
96
|
+
of the License, and in such case Affirmer hereby affirms that he or she
|
|
97
|
+
will not (i) exercise any of his or her remaining Copyright and Related
|
|
98
|
+
Rights in the Work or (ii) assert any associated claims and causes of
|
|
99
|
+
action with respect to the Work, in either case contrary to Affirmer's
|
|
100
|
+
express Statement of Purpose.
|
|
101
|
+
|
|
102
|
+
4. Limitations and Disclaimers.
|
|
103
|
+
|
|
104
|
+
a. No trademark or patent rights held by Affirmer are waived, abandoned,
|
|
105
|
+
surrendered, licensed or otherwise affected by this document.
|
|
106
|
+
b. Affirmer offers the Work as-is and makes no representations or
|
|
107
|
+
warranties of any kind concerning the Work, express, implied,
|
|
108
|
+
statutory or otherwise, including without limitation warranties of
|
|
109
|
+
title, merchantability, fitness for a particular purpose, non
|
|
110
|
+
infringement, or the absence of latent or other defects, accuracy, or
|
|
111
|
+
the present or absence of errors, whether or not discoverable, all to
|
|
112
|
+
the greatest extent permissible under applicable law.
|
|
113
|
+
c. Affirmer disclaims responsibility for clearing rights of other persons
|
|
114
|
+
that may apply to the Work or any use thereof, including without
|
|
115
|
+
limitation any person's Copyright and Related Rights in the Work.
|
|
116
|
+
Further, Affirmer disclaims responsibility for obtaining any necessary
|
|
117
|
+
consents, permissions or other rights required for any use of the
|
|
118
|
+
Work.
|
|
119
|
+
d. Affirmer understands and acknowledges that Creative Commons is not a
|
|
120
|
+
party to this document and has no duty or obligation with respect to
|
|
121
|
+
this CC0 or use of the Work.
|
data/LICENSES/MIT.txt
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) <year> <copyright holders>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
|
6
|
+
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
|
7
|
+
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
|
9
|
+
following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial
|
|
12
|
+
portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
|
15
|
+
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
|
16
|
+
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
17
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
|
18
|
+
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
3
|
+
|
|
4
|
+
SPDX-License-Identifier: CC-BY-SA-4.0
|
|
5
|
+
-->
|
|
6
|
+
|
|
7
|
+
# Tokra
|
|
8
|
+
|
|
9
|
+
TODO: Delete this and the text below, and describe your gem
|
|
10
|
+
|
|
11
|
+
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/tokra`. To experiment with that code, run `bin/console` for an interactive prompt.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
|
|
16
|
+
|
|
17
|
+
Install the gem and add to the application's Gemfile by executing:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
TODO: Write usage instructions here
|
|
32
|
+
|
|
33
|
+
## Development
|
|
34
|
+
|
|
35
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
36
|
+
|
|
37
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
38
|
+
|
|
39
|
+
## Contributing
|
|
40
|
+
|
|
41
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/tokra. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/tokra/blob/main/CODE_OF_CONDUCT.md).
|
|
42
|
+
|
|
43
|
+
## Code of Conduct
|
|
44
|
+
|
|
45
|
+
Everyone interacting in the Tokra project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/tokra/blob/main/CODE_OF_CONDUCT.md).
|
data/README.rdoc
ADDED
data/REUSE.toml
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
version = 1
|
|
2
|
+
|
|
3
|
+
[[annotations]]
|
|
4
|
+
path = 'Gemfile.lock'
|
|
5
|
+
SPDX-FileCopyrightText = "2025 Kerrick Long <me@kerricklong.com>"
|
|
6
|
+
SPDX-License-Identifier = "CC0-1.0"
|
|
7
|
+
|
|
8
|
+
[[annotations]]
|
|
9
|
+
path = 'README.rdoc'
|
|
10
|
+
SPDX-FileCopyrightText = "2026 Kerrick Long <me@kerricklong.com>"
|
|
11
|
+
SPDX-License-Identifier = "CC-BY-SA-4.0"
|
data/Rakefile
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
|
|
6
|
+
require "bundler/gem_tasks"
|
|
7
|
+
|
|
8
|
+
require "rdoc/task"
|
|
9
|
+
RDoc::Task.new do |rdoc|
|
|
10
|
+
rdoc.rdoc_dir = ENV["RDOC_OUTPUT"] || "tmp/rdoc"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Rust extension compilation
|
|
14
|
+
require "rb_sys/extensiontask"
|
|
15
|
+
|
|
16
|
+
GEMSPEC = Gem::Specification.load("tokra.gemspec")
|
|
17
|
+
|
|
18
|
+
RbSys::ExtensionTask.new("tokra", GEMSPEC) do |ext|
|
|
19
|
+
ext.lib_dir = "lib/tokra"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
task build: :compile
|
|
23
|
+
|
|
24
|
+
# Import all tasks from the tasks/ directory
|
|
25
|
+
Dir.glob("tasks/*.rake").each { |r| import r }
|
|
26
|
+
|
|
27
|
+
task default: %w[compile lint:fix test lint steep lint:rust]
|
data/Steepfile
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
|
|
6
|
+
target :lib do
|
|
7
|
+
signature "sig"
|
|
8
|
+
check "lib"
|
|
9
|
+
|
|
10
|
+
library "pathname"
|
|
11
|
+
library "fileutils"
|
|
12
|
+
library "minitest"
|
|
13
|
+
library "date"
|
|
14
|
+
library "timeout"
|
|
15
|
+
end
|
data/clippy.toml
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
# Approved Clippy allow exceptions.
|
|
9
|
+
# Each entry must explain WHY the exception is unavoidable.
|
|
10
|
+
# Review carefully before adding new entries.
|
|
11
|
+
|
|
12
|
+
allow "ext/tokra/src/lib.rs", line: 7, reason: <<~END
|
|
13
|
+
Magnus FFI requires owned types at the Ruby-Rust boundary. Ruby strings must be
|
|
14
|
+
converted to owned Rust Strings when crossing the FFI, as borrowed references
|
|
15
|
+
cannot safely outlive the Ruby GC cycle. This is a fundamental constraint of
|
|
16
|
+
the magnus crate's type marshaling, not a code smell to be fixed.
|
|
17
|
+
END
|
|
18
|
+
|
|
19
|
+
allow "ext/tokra/src/lib.rs", lines: [77, 189, 249], lint: "unsafe_code", reason: <<~END
|
|
20
|
+
Tauri Pattern: unsafe impl Send for RbEventLoop, RbWindow, RbWebView
|
|
21
|
+
|
|
22
|
+
These types wrap platform-native GUI objects (tao::Window, wry::WebView) that
|
|
23
|
+
are bound to the main OS thread. Magnus's TypedData trait requires Send, but
|
|
24
|
+
these types are not inherently thread-safe.
|
|
25
|
+
|
|
26
|
+
INVARIANT: All access to these types happens exclusively on the main thread
|
|
27
|
+
via the tao event loop. Ruby code cannot access these objects from Worker
|
|
28
|
+
Ractors - only through the Send-safe Proxy which marshals commands to the
|
|
29
|
+
main thread.
|
|
30
|
+
|
|
31
|
+
This pattern is identical to how Tauri implements this in tauri-runtime-wry:
|
|
32
|
+
- `unsafe impl Send for WindowsStore {}` (lib.rs:433)
|
|
33
|
+
- `unsafe impl Send for DispatcherMainThreadContext<T> {}` (lib.rs:451)
|
|
34
|
+
- `unsafe impl Send for WindowBuilderWrapper {}` (lib.rs:809)
|
|
35
|
+
|
|
36
|
+
The invariant is enforced by:
|
|
37
|
+
1. The event loop callback runs only on the main thread
|
|
38
|
+
2. Ractors can only hold the Proxy (which IS genuinely Send+Sync-safe)
|
|
39
|
+
3. Integration tests verify invariants:
|
|
40
|
+
test/integration/thread_safety_invariant_test.rb:23-38 (Proxy is Ractor-safe)
|
|
41
|
+
test/integration/thread_safety_invariant_test.rb:44-55 (main thread instantiation)
|
|
42
|
+
test/integration/thread_safety_invariant_test.rb:60-73 (main thread operations)
|
|
43
|
+
END
|
|
44
|
+
|
|
45
|
+
allow "ext/tokra/src/lib.rs", lines: [310], lint: "unsafe_code", reason: <<~END
|
|
46
|
+
Magnus TypedData for RbProxy: unsafe impl TypedData for RbProxy
|
|
47
|
+
|
|
48
|
+
This is SAFE because EventLoopProxy<T> is genuinely Send+Sync by design.
|
|
49
|
+
We use this to enable the `frozen_shareable` flag which makes the Ruby
|
|
50
|
+
object Ractor-shareable when frozen.
|
|
51
|
+
|
|
52
|
+
Unlike the other unsafe impls (which are for main-thread-only types),
|
|
53
|
+
this one is actually safe - tao explicitly designed EventLoopProxy for
|
|
54
|
+
cross-thread communication. The unsafe is only required by Magnus's
|
|
55
|
+
TypedData trait signature, not because the operation is unsafe.
|
|
56
|
+
|
|
57
|
+
VERIFIED BY: test/integration/thread_safety_invariant_test.rb:23-38
|
|
58
|
+
Tests that frozen Proxy becomes Ractor-shareable.
|
|
59
|
+
END
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
3
|
+
|
|
4
|
+
SPDX-License-Identifier: CC-BY-SA-4.0
|
|
5
|
+
-->
|
|
6
|
+
|
|
7
|
+
# ADR 001: Tokra Architecture - Ruby-Host with Thin Rust Bindings
|
|
8
|
+
|
|
9
|
+
## Status
|
|
10
|
+
Accepted
|
|
11
|
+
|
|
12
|
+
## Date
|
|
13
|
+
2026-01-29
|
|
14
|
+
|
|
15
|
+
## Context
|
|
16
|
+
We are building **Tokra**, a desktop application framework for Ruby 4.0.1+.
|
|
17
|
+
The goal is to enable developers to build performant, native-feeling desktop apps using HTML/CSS/JS for the frontend and **Pure Ruby** for the backend logic.
|
|
18
|
+
|
|
19
|
+
Historically, frameworks like Tauri required significant Rust code to manage state, commands, and sidecars. However, with the release of Ruby 4.0 (Ractors, YJIT), Ruby is now fast enough and concurrent enough to handle these responsibilities directly.
|
|
20
|
+
|
|
21
|
+
We need an architecture that:
|
|
22
|
+
1. Minimizes Rust maintenance overhead (cost & complexity).
|
|
23
|
+
2. Maximizes Ruby developer experience (logic stays in Ruby).
|
|
24
|
+
3. Ensures 60FPS UI performance by never blocking the main thread.
|
|
25
|
+
4. Maintains a secure barrier between the WebView and the System.
|
|
26
|
+
|
|
27
|
+
## Decision
|
|
28
|
+
We will adopt the **"Thin Binding" + "Ruby Host"** architecture.
|
|
29
|
+
|
|
30
|
+
1. **Rust Layer (The Dumb Pipe):** We will use `magnus` to create a thin, un-opinionated wrapper around `tao` (Windowing) and `wry` (WebView). It will not know about "commands" or "state"; it will strictly pass raw string messages to Ruby.
|
|
31
|
+
2. **Ruby Layer (The Brain):** The application lifecycle, command routing, security allowlists, and state management will be implemented entirely in Ruby.
|
|
32
|
+
3. **Concurrency Model (Ractor Isolation):**
|
|
33
|
+
* **Main Ractor:** Dedicated exclusively to the `Tao` event loop and Window management.
|
|
34
|
+
* **Worker Ractor:** Dedicated to user business logic, file I/O, and heavy computation.
|
|
35
|
+
* **IPC:** Communication between layers uses `Ractor.send(obj, move: true)` for zero-copy overhead.
|
|
36
|
+
|
|
37
|
+
## Architecture Specification
|
|
38
|
+
|
|
39
|
+
### 1. High-Level Diagram
|
|
40
|
+
|
|
41
|
+
[ WebView (HTML/JS) ]
|
|
42
|
+
|
|
|
43
|
+
| IPC (Raw JSON String)
|
|
44
|
+
v
|
|
45
|
+
[ Rust Layer (TokraFFI) ] <-- "Dumb Pipe"
|
|
46
|
+
|
|
|
47
|
+
| Callback
|
|
48
|
+
v
|
|
49
|
+
[ Main Ractor (UI) ] <-- "The Window Manager"
|
|
50
|
+
| 1. Security Check (Allowlist)
|
|
51
|
+
| 2. Ractor.send(move: true)
|
|
52
|
+
v
|
|
53
|
+
[ Worker Ractor (Logic) ] <-- "The User App"
|
|
54
|
+
| 1. Command Routing
|
|
55
|
+
| 2. Business Logic / DB / IO
|
|
56
|
+
|
|
|
57
|
+
| proxy.send_event()
|
|
58
|
+
v
|
|
59
|
+
[ Main Ractor (UI) ] <-- Wakes up Event Loop
|
|
60
|
+
|
|
|
61
|
+
v
|
|
62
|
+
[ Rust Layer (eval_js) ]
|
|
63
|
+
|
|
|
64
|
+
v
|
|
65
|
+
[ WebView (Update UI) ]
|
|
66
|
+
|
|
67
|
+
### 2. Rust Deliverables (`ext/tokra/src/lib.rs`)
|
|
68
|
+
|
|
69
|
+
The Rust extension must expose three primary classes via Magnus. It should use `cdylib` crate type.
|
|
70
|
+
|
|
71
|
+
#### A. `Tokra::Native::EventLoop`
|
|
72
|
+
* **Responsibility:** Hijack the OS Main Thread for `tao`.
|
|
73
|
+
* **Signature:** `run(&self, callback: Proc)` -> `!` (Never returns)
|
|
74
|
+
* **Signal Safety:** Must implement a `ctrlc` handler that triggers `UserEvent::Exit` to wake the loop on SIGINT. Ruby's `Signal.trap` will not work while Tao is running.
|
|
75
|
+
* **Behavior:**
|
|
76
|
+
* Iterates `event_loop.run_return` or `run`.
|
|
77
|
+
* On `UserEvent` (from Proxy), triggers the callback.
|
|
78
|
+
* On `WindowEvent` (Resize, Close), triggers the callback.
|
|
79
|
+
|
|
80
|
+
#### B. `Tokra::Native::Window`
|
|
81
|
+
* **Responsibility:** Wrap `tao::window::Window`.
|
|
82
|
+
* **Methods:** `new`, `set_title`, `set_size`, `id`.
|
|
83
|
+
|
|
84
|
+
#### C. `Tokra::Native::WebView`
|
|
85
|
+
* **Responsibility:** Wrap `wry::WebView`.
|
|
86
|
+
* **Methods:**
|
|
87
|
+
* `new(window, url, ipc_callback)`
|
|
88
|
+
* `eval(js_string)`
|
|
89
|
+
* **Security Detail:** The `ipc_callback` passed here is the "Border Crossing". Rust blindly fires this Proc whenever the WebView emits a message.
|
|
90
|
+
|
|
91
|
+
#### D. `Tokra::Native::Proxy`
|
|
92
|
+
* **Responsibility:** Thread-safe handle to wake up the loop.
|
|
93
|
+
* **Methods:** `wake_up(payload)`
|
|
94
|
+
* **Implementation:** Wraps `tao::event_loop::EventLoopProxy`. This is critical for Ractors to talk back to the UI.
|
|
95
|
+
|
|
96
|
+
### 3. Ruby Implementation (`lib/tokra.rb`)
|
|
97
|
+
|
|
98
|
+
The Ruby gem bootstraps the Ractor model.
|
|
99
|
+
|
|
100
|
+
#### The Main Ractor (UI Thread)
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
module Tokra
|
|
104
|
+
class App
|
|
105
|
+
def self.run(initial_url)
|
|
106
|
+
# 1. Create communication ports
|
|
107
|
+
ipc_port = Ractor::Port.new # Main receives IPC from WebView
|
|
108
|
+
response_port = Ractor::Port.new # Main receives responses from Worker
|
|
109
|
+
|
|
110
|
+
# 2. Setup the logic worker
|
|
111
|
+
worker = Ractor.new(response_port) do |resp_port|
|
|
112
|
+
loop do
|
|
113
|
+
msg = Ractor.receive
|
|
114
|
+
result = yield(msg) # User's logic runs here
|
|
115
|
+
resp_port << result
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# 3. Setup Rust Windowing (Main Thread)
|
|
120
|
+
event_loop = Tokra::Native::EventLoop.new
|
|
121
|
+
proxy = event_loop.create_proxy
|
|
122
|
+
|
|
123
|
+
# 4. Define the IPC "Bridge" (must be shareable)
|
|
124
|
+
ipc_handler = Ractor.shareable_proc do |raw_msg|
|
|
125
|
+
ipc_port << raw_msg
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
window = Tokra::Native::Window.new(event_loop)
|
|
129
|
+
webview = Tokra::Native::WebView.new(window, initial_url, ipc_handler)
|
|
130
|
+
|
|
131
|
+
# 5. Block forever on Main Thread
|
|
132
|
+
event_loop.run do |event|
|
|
133
|
+
case event
|
|
134
|
+
when Tokra::Native::IpcEvent
|
|
135
|
+
# Security: Validate before forwarding
|
|
136
|
+
worker << { type: :ipc, payload: event.message, proxy: proxy }
|
|
137
|
+
when Tokra::Native::WakeUpEvent
|
|
138
|
+
# Response from worker - send to WebView
|
|
139
|
+
webview.eval("window.__tokra_callback(#{event.payload.to_json})")
|
|
140
|
+
when Tokra::Native::WindowCloseEvent
|
|
141
|
+
break
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
#### The Security Implementation
|
|
150
|
+
|
|
151
|
+
Security is enforced in **Ruby**, not Rust.
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
class SecurityGuard
|
|
155
|
+
ALLOWLIST = {
|
|
156
|
+
"fs_read" => ->(path) { path.start_with?(APP_DATA_DIR) },
|
|
157
|
+
"net_req" => ->(url) { url.start_with?("https://api.myapp.com") }
|
|
158
|
+
}.freeze
|
|
159
|
+
|
|
160
|
+
def self.authorize!(command, args)
|
|
161
|
+
raise SecurityError, "Access Denied: #{command}" unless ALLOWLIST[command]&.call(args)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### 4. Build System Specification
|
|
167
|
+
* **Tooling:** Must use `rb_sys` gem to bridge Cargo and Make.
|
|
168
|
+
* **Manifest:** `ext/tokra/extconf.rb` must use `create_rust_makefile("tokra/tokra")`.
|
|
169
|
+
* **Extension:** The compiled binary must be reachable via `require "tokra/tokra"`.
|
|
170
|
+
|
|
171
|
+
## Consequences
|
|
172
|
+
|
|
173
|
+
### Positive
|
|
174
|
+
* **Dev Velocity:** Logic changes happen in Ruby (Instant reload potential). Rust compilation is rare.
|
|
175
|
+
* **Cost:** Rust contractor scope is fixed and small (~500 LOC).
|
|
176
|
+
* **Performance:** UI is unblockable. Ractor move semantics avoid GC pressure during IPC.
|
|
177
|
+
|
|
178
|
+
### Negative
|
|
179
|
+
* **Complexity:** Ractor message passing is strictly async. The user must write async-aware Ruby code (promises/callbacks).
|
|
180
|
+
* **Signals:** Standard Ruby signal trapping (Ctrl+C) is disabled; we rely on the Rust layer to catch OS signals.
|
|
181
|
+
* **Memory:** Minimum 2 Ractors means slightly higher base memory usage than a single-threaded app.
|
|
182
|
+
* **Packaging:** Requires bundling the compiled `.so`/`.dll` inside the gem.
|
|
183
|
+
|
|
184
|
+
## References
|
|
185
|
+
* [Magnus Documentation](https://docs.rs/magnus)
|
|
186
|
+
* [Ruby 4.0 Ractor Guide](https://docs.ruby-lang.org/en/master/ractor.md)
|
|
187
|
+
* [Wry IPC Interface](https://docs.rs/wry/latest/wry/struct.WebViewBuilder.html#method.with_ipc_handler)
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
3
|
+
|
|
4
|
+
SPDX-License-Identifier: CC-BY-SA-4.0
|
|
5
|
+
-->
|
|
6
|
+
|
|
7
|
+
# ADR 002: Tokra Rails - The Rack Adapter & "Serverless" Desktop
|
|
8
|
+
|
|
9
|
+
## Status
|
|
10
|
+
Accepted
|
|
11
|
+
|
|
12
|
+
## Date
|
|
13
|
+
2026-01-29
|
|
14
|
+
|
|
15
|
+
## Context
|
|
16
|
+
Developers want to use **Ruby on Rails** to build Tokra desktop applications.
|
|
17
|
+
The traditional approach (Electron) involves spawning a child process running a TCP web server (like Puma) and pointing the WebView to `http://localhost:3000`.
|
|
18
|
+
|
|
19
|
+
This approach has significant downsides:
|
|
20
|
+
1. **Security:** It opens a TCP port on the user's machine, which other processes can sniff.
|
|
21
|
+
2. **Performance:** It incurs TCP/IP loopback overhead.
|
|
22
|
+
3. **Management:** Dealing with "orphan" server processes when the app closes is difficult.
|
|
23
|
+
|
|
24
|
+
We need a way to run Rails *inside* the Tokra process, leveraging the Ruby 4.0 architecture defined in [ADR 001].
|
|
25
|
+
|
|
26
|
+
## Decision
|
|
27
|
+
We will implement **Tokra Rails** as a direct Rack Adapter over a Wry Custom Protocol.
|
|
28
|
+
|
|
29
|
+
We will **not** bundle Puma or any TCP server. Instead, we will treat the WebView's resource loader as the "Web Server" and the Rails App as a library function call.
|
|
30
|
+
|
|
31
|
+
## The "100 Lines" Architecture
|
|
32
|
+
|
|
33
|
+
### 1. The Mechanism: Custom Protocol (`rails://`)
|
|
34
|
+
Instead of IPC (which is for JSON messages), we will use Wry's **Custom Protocol** feature.
|
|
35
|
+
* **Rust Layer:** We add *one* generic method to our FFI: `register_protocol(scheme, ruby_proc)`.
|
|
36
|
+
* **The Flow:**
|
|
37
|
+
1. Frontend requests `rails://app/posts/1` (via Turbo, HTMX, or `fetch`).
|
|
38
|
+
2. Rust intercepts the request.
|
|
39
|
+
3. Rust passes the HTTP verb, headers, and body to the **Main Ractor** (Ruby).
|
|
40
|
+
4. Main Ractor sends it to the **Rails Ractor**.
|
|
41
|
+
5. Rails Ractor runs `MyApp::Application.call(env)`.
|
|
42
|
+
6. The response (HTML/JSON) is handed back to Rust.
|
|
43
|
+
|
|
44
|
+
### 2. The Ruby Code (`lib/tokra/rails/handler.rb`)
|
|
45
|
+
|
|
46
|
+
This is the "100 lines" of Ruby. It is a standard `Rack::Handler` that translates the Tokra Protocol Struct into a Rack Environment Hash.
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
# frozen_string_literal: true
|
|
50
|
+
|
|
51
|
+
module Rack
|
|
52
|
+
module Handler
|
|
53
|
+
class Tokra
|
|
54
|
+
def self.run(app, options = {})
|
|
55
|
+
# The protocol handler must be a shareable proc since it's
|
|
56
|
+
# called from Rust across potential Ractor boundaries
|
|
57
|
+
handler = Ractor.shareable_proc do |req|
|
|
58
|
+
# 1. Map Request -> Rack Env
|
|
59
|
+
env = {
|
|
60
|
+
"REQUEST_METHOD" => req.method,
|
|
61
|
+
"SCRIPT_NAME" => "",
|
|
62
|
+
"PATH_INFO" => req.path,
|
|
63
|
+
"QUERY_STRING" => req.query,
|
|
64
|
+
"SERVER_NAME" => "tokra",
|
|
65
|
+
"SERVER_PORT" => "80",
|
|
66
|
+
"rack.version" => Rack::VERSION,
|
|
67
|
+
"rack.input" => StringIO.new(req.body),
|
|
68
|
+
"rack.errors" => $stderr,
|
|
69
|
+
"rack.multithread" => false, # Requests are serialized through protocol handler
|
|
70
|
+
"rack.run_once" => false,
|
|
71
|
+
"rack.url_scheme" => "rails"
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# 2. Call Rails (In-Memory)
|
|
75
|
+
status, headers, body_proxy = app.call(env)
|
|
76
|
+
|
|
77
|
+
# 3. Map Response -> Rust Response
|
|
78
|
+
body_string = +""
|
|
79
|
+
body_proxy.each { |chunk| body_string << chunk }
|
|
80
|
+
body_proxy.close if body_proxy.respond_to?(:close)
|
|
81
|
+
|
|
82
|
+
# Return to Rust
|
|
83
|
+
{ status: status, headers: headers, body: body_string }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Register the "rails://" protocol hook in Rust
|
|
87
|
+
::Tokra::Native.register_protocol("rails", &handler)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### 3. The JS Code (`10 lines`)
|
|
95
|
+
We need a tiny shim to ensure relative links work and Turbo behaves correctly with the custom protocol.
|
|
96
|
+
|
|
97
|
+
// app/javascript/tokra_shim.js
|
|
98
|
+
document.addEventListener("turbo:before-fetch-request", (event) => {
|
|
99
|
+
// Ensure Turbo knows we are using a custom protocol
|
|
100
|
+
// (Turbo 8+ handles non-HTTP protocols gracefully usually)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
// Optional: Bridge for "Server Push" via standard IPC
|
|
104
|
+
window.Tokra.on("rails:stream", (html) => {
|
|
105
|
+
Turbo.renderStreamMessage(html)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
## Integration with Solid* & SQLite
|
|
109
|
+
|
|
110
|
+
Because the Rails app runs inside the **Worker Ractor** (defined in ADR 001), strict isolation rules apply:
|
|
111
|
+
|
|
112
|
+
1. **SQLite Ownership:** `SQLite3::Database` objects are NOT Ractor-shareable.
|
|
113
|
+
* **Constraint:** You cannot establish the DB connection in the Main Ractor and pass it down.
|
|
114
|
+
* **Implementation:** The Rails app must lazy-load the connection (`ActiveRecord::Base.establish_connection`) *only* after the Worker Ractor has started.
|
|
115
|
+
2. **Solid Queue:** The background worker threads for Solid Queue spawn *inside* the Worker Ractor (or we spawn a dedicated `Queue Ractor`).
|
|
116
|
+
3. **Solid Cache:** Writes to the local SQLite DB.
|
|
117
|
+
|
|
118
|
+
## Consequences
|
|
119
|
+
|
|
120
|
+
### Positive
|
|
121
|
+
* **Zero Latency:** No TCP handshake. Request/Response is a memory copy (or move).
|
|
122
|
+
* **Native Assets:** Images/CSS served via `rails://app/assets/...` work exactly like standard web pages.
|
|
123
|
+
* **Standard Rails:** Developers use `routes.rb`, Controllers, and Views exactly as normal. No "API-only" mode required.
|
|
124
|
+
|
|
125
|
+
### Negative
|
|
126
|
+
* **Streaming Limitations:** The `rails://` custom protocol does NOT support `ActionController::Live` or Turbo Streams (chunked responses).
|
|
127
|
+
* **Mitigation:** Streaming features must use the IPC Bridge (`Tokra.on('stream')`) instead of the protocol handler.
|
|
128
|
+
* **Cookies:** The WebView manages cookies for the custom protocol, but we must ensure `set-cookie` headers are respected by Wry's custom protocol implementation.
|
|
129
|
+
|
|
130
|
+
## Validation
|
|
131
|
+
* **Is it 100 lines?** Yes. The Rack Handler logic is mostly hash mapping.
|
|
132
|
+
* **Does Solid work?** Yes. Solid is just Ruby code and SQL. It doesn't know it's not running in a Puma process.
|