Compare commits

...

340 Commits

Author SHA1 Message Date
Yukihiro "Matz" Matsumoto 735b94535c numeric.c: raise TypeError for non-Integer bitwise operands
Integer#&, #| and #^ read the right-hand operand with mrb_integer()
without checking its type. For a Float (or any non-Integer) this reads an
unrelated union field and returns a garbage value instead of raising, as
CRuby does. Raise TypeError via mrb_int_noconv() when the operand is not
an Integer. Bigint operands are still handled before this point, and the
shift operators keep coercing their width as before.

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-05 11:23:43 +09:00
Yukihiro "Matz" Matsumoto 9982bc1223 random.c: fix signed integer overflow in rand() with integer ranges
rand_range_int() computed `end - begin + 1` in mrb_int before checking
for a reversed or empty range. For extreme bounds the subtraction itself
overflowed mrb_int (UndefinedBehaviorSanitizer signed-integer-overflow),
and the wraparound could turn a reversed range into a positive span,
defeating the guard.

Reject reversed or empty ranges before subtracting, and compute the
candidate count in unsigned arithmetic, so no signed overflow is
possible. A new unsigned uniform sampler draws from the full mrb_int
domain, so valid ranges whose width exceeds MRB_INT_MAX now return a
uniform in-range value instead of nil, matching CRuby.

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-05 11:23:43 +09:00
Yukihiro "Matz" Matsumoto 24f428af5e Merge pull request #6879 from Asmod4n/method_alias
Add alias support for mruby-method and mruby-proc-ext
2026-06-05 07:43:35 +09:00
Asmod4n a6168fa111 Merge branch 'method_alias' of https://github.com/Asmod4n/mruby into method_alias 2026-06-04 17:37:12 +02:00
Asmod4n d8911416c1 add alias support to mruby-method and mruby-prox-ext 2026-06-04 17:36:41 +02:00
Yukihiro "Matz" Matsumoto b0e7118e72 Merge pull request #6878 from mruby/dependabot/bundler/bundler-dependencies-6264482842
build(deps): bump yard from 0.9.43 to 0.9.44 in the bundler-dependencies group
2026-06-03 10:46:57 +09:00
dependabot[bot] 4073ad386c build(deps): bump yard in the bundler-dependencies group
Bumps the bundler-dependencies group with 1 update: [yard](https://yardoc.org).


Updates `yard` from 0.9.43 to 0.9.44

---
updated-dependencies:
- dependency-name: yard
  dependency-version: 0.9.44
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: bundler-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-02 20:17:23 +00:00
Yukihiro "Matz" Matsumoto c58b0b66ba Merge pull request #6877 from dearblue/array-ext
Free the index array immediately at the end of `ary_combination_next()`
2026-06-02 07:07:56 +09:00
dearblue 6704a385b7 Free the index array immediately at the end of ary_combination_next() 2026-06-01 22:27:59 +09:00
Yukihiro "Matz" Matsumoto 72e5ebed55 AUTHORS: update entries [ci skip] 2026-06-01 14:15:29 +09:00
Yukihiro "Matz" Matsumoto 9d084b09b7 mruby-io: write literal strings directly via fd_write_buf
io_puts_str, io_puts_ary, and io_puts allocated a fresh mruby String
for every "\n", empty-array marker, "[...]" overflow marker, and
no-arg newline, only for fd_write to unpack it back to ptr/len.
The allocations also stayed on the GC arena across the recursive
walk, scaling pressure with array length.

Split fd_write into fd_write_buf (the EINTR-resilient write loop
over ptr/len) plus the existing mrb_value wrapper, and add a
FD_WRITE_LIT macro for compile-time-known literals. Replace the
four mrb_str_new_lit + fd_write pairs with FD_WRITE_LIT. The "" s
"" inside the macro enforces that the argument is a string literal
so sizeof(s) - 1 is the correct length.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-31 09:09:20 +09:00
Yukihiro "Matz" Matsumoto f5ca906852 mruby-compiler: fix bare nil? in if/unless to use self as receiver
The if/unless nil? optimization called codegen() on the call node's
receiver, but a bare `nil?` is parsed as an FCALL whose receiver is
NULL. codegen(NULL) emits OP_LOADNIL, so the JMPNIL was testing the
literal nil instead of self, making `if nil?` always behave as
`if nil.nil?` (always true) and `unless nil?` always skip its body.
Load self when the receiver is implicit. Fixes #6874.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-30 07:39:45 +09:00
Yukihiro "Matz" Matsumoto 543bc325f3 Merge pull request #6875 from mruby/dependabot/github_actions/github-actions-dependencies-754f0868f5
build(deps): bump github/codeql-action from 4.35.5 to 4.36.0 in the github-actions-dependencies group
2026-05-30 07:29:35 +09:00
dependabot[bot] 828efa9b87 build(deps): bump github/codeql-action
Bumps the github-actions-dependencies group with 1 update: [github/codeql-action](https://github.com/github/codeql-action).


Updates `github/codeql-action` from 4.35.5 to 4.36.0
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/9e0d7b8d25671d64c341c19c0152d693099fb5ba...7211b7c8077ea37d8641b6271f6a365a22a5fbfa)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.36.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-29 17:56:25 +00:00
Yukihiro "Matz" Matsumoto 94bd329afe Merge pull request #6871 from hasumikin/fix/execute_task
Fix execute_task() so unhandled task exceptions become task results
2026-05-29 07:05:36 +09:00
Yukihiro "Matz" Matsumoto be36b67a12 mruby-task: clear dead stack slots when marking preempted tasks
mrb_task_mark_all marked a task's live registers but, unlike
mark_context_stack in gc.c, never cleared the slots above the live
range. When a preempted task's live range later shrank (a frame had
returned), the stale object pointers left in those slots were neither
marked nor cleared: the objects were swept while the pointers survived.
Re-entering the same frame reused those slots, and the next mark of the
resumed task hit a freed object, tripping the MRB_TT_FREE assertion in
mrb_gc_mark.

Clear the dead slots after marking, exactly as mark_context_stack does
for the running context.

Fixes #6870.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-29 05:59:14 +09:00
HASUMI Hitoshi a502be4f9f Fix fallback when abnormal error happens
It is not likely happens but if happened, in pattern 1, the whole
process abort when mrb->jmp is NULL.
Instead, make the status MRB_TASK_STOPPED and delegate following
logic of "Handle task termination"
2026-05-29 01:30:24 +09:00
HASUMI Hitoshi f1232334c0 Fix execute_task() so unhandled task exceptions become task results
This patch fixes a bug that c09196c introduced.

## Background

`mrb_task_run()` has two usage patterns:

1. Called directly from `main()` as the top-level scheduler (PicoRuby and R2P2). There is no surrounding C exception handler, so mrb->jmp is NULL on entry
2. Called from Ruby code via Task.run, bootstrapped on top of mruby's regular call chain. mrb->jmp is non-NULL

Historically, an unhandled exception raised inside a task body was turned into the task's result value by `mrb_vm_exec()`: the L_RAISE path walked callinfo down to cibase, ran `fiber_terminate()`, and - because c->vmexec was TRUE and prev_jmp was NULL in pattern 1 - took `return mrb_obj_value(mrb->exc)`.
That value landed in t->result and could be read back through `mrb_task_value()` / `join()`.

## What c09196c broke

It consider only pattern 2 and wrapped `mrb_task_run()` in a protect frame (MRB_TRY / mrb_protect_error) to guarantee that loop_running is cleared on exception.
As a side effect, mrb->jmp is now always non-NULL while a task body is executing, so the L_RAISE path takes `MRB_THROW(prev_jmp)` instead of returning the exception value.

In pattern 2 this merely changed the semantics (exceptions started propagating out of `Task.run` instead of being stored as task results).
In pattern 1 it was FATAL: the throw unwound to mrb_task_run's catch handler, which called `mrb_exc_raise()` to re-propagate, and with no outer jmpbuf this aborted the process.
PicoRuby/R2P2 could no longer retrieve task exceptions via `mrb_task_value()`.

## Fix

Restore the "task exception becomes task result" contract uniformly for both patterns, independent of mrb->jmp:

* Add `mrb_task_state.exception_as_result`. When set, `mrb_vm_exec()`'s non-root_c L_RAISE branch returns the exception as a value even if prev_jmp is non-NULL, instead of throwing

* `execute_task_vm()` raises the flag around `mrb_vm_exec()`, captures the exception into `t->result`, and clears `mrb->exc`

* Wrap `execute_task_vm()` in `mrb_protect_error()` as a safety net for rare paths that still unwind via MRB_THROW (e.g. CINFO_SKIP frames). exception_as_result is reset both at the end of the body and immediately after `mrb_protect_error()` returns, so a caught throw does not leave the llag set

* Expose `Task#value` to retrieve t->result from Ruby, since Task#join cannot deliver the value through its return path under cooperative scheduling

* Add a test asserting that `Task#join` on a task that raised returns the exception object, matching the pre-c09196c observable behavior

## Notes

The "task exception becomes task result" semantics match the mruby/c's rrt0.c and the spirit of CRuby's Thread (an unhandled exception in a thread does not kill the scheduler / process; it surfaces when the thread is joined).
The visible API shape still differs from CRuby - `Task#join` here returns the exception object rather than re-raising it - but the scheduler is no longer destabilized by task errors in either invocation pattern.
2026-05-29 01:25:47 +09:00
Yukihiro "Matz" Matsumoto dad7c0fa36 Merge pull request #6869 from hasumikin/fix/task
Separate the union of timeslice and result in struct mrb_task
2026-05-28 20:20:43 +09:00
HASUMI Hitoshi e0d6d39ce9 Write validate flag (status) after setting timeslice
`mrb_tick()` observes `status == RUNNING` before touching `timeslice`,
so initializing `timeslice` first avoids exposing a partially initialized running state
2026-05-28 19:18:17 +09:00
HASUMI Hitoshi 564726c44f t->result now can be marked unconditionally 2026-05-28 19:16:04 +09:00
HASUMI Hitoshi 5e669560eb Amend field position to pack effectively 2026-05-28 19:07:20 +09:00
HASUMI Hitoshi 030a08092a Separate the union of timeslice and result in struct mrb_task
Bug scenario:
* VM returns an Exception `t->state.result = mrb_vm_exec(...);`
* Despite task is still MRB_TASK_STATUS_RUNNING, IRQ triggered by chance and `mrb_tick()` executes `t->state.timeslice--;`
* But the same memory area already holds the `result`, `timeslice--` reduces `result.value.p`'s top byte

The fix is to separate `timeslice` and `result` into different fields.
I have considered improving critical sections, but I ended up with this patch because I believe it is widely effective and less error-prone.
2026-05-28 18:48:13 +09:00
Yukihiro "Matz" Matsumoto d2a1c43a5f vm.c: defer task switch across C call boundary
A task switch (early return from mrb_vm_exec) is unsafe when execution
has re-entered the VM from C (mrb_funcall, mrb_yield, mrb_vm_run). The
C stack frame between the scheduler's mrb_vm_exec and the current frame
cannot be suspended, and returning early from the inner mrb_vm_exec
leaves the call-info stack drifted, so the enclosing mrb_vm_run trips
its `c->ci == c->cibase || ...` assertion (or corrupts state in a
non-debug build).

This happens when a block yielded from a C function wakes a task, for
example Task::Queue#push from inside a block passed to a C method via
mrb_yield_argv.

Defer the switch via task_across_c_boundary, which walks the call-info
stack for a C frame (cci > 0), mirroring the cooperative guard in
Task.pass. The switch resumes once execution unwinds back to a frame
with no C boundary. The gc.iterating short-circuit from #6862 is kept
as a cheap pre-check.

Fixes #6868.
Refs #6864.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-28 15:36:21 +09:00
Yukihiro "Matz" Matsumoto dc671f007a vm.c: restore mrb->jmp on early return for task switch
mrb_vm_exec sets mrb->jmp to its own stack-local c_jmp on entry and
restores the caller's prev_jmp on every normal return path. The early
return added for task switching (RETURN_IF_TASK_STOPPED) skipped that
restore, so after a task was preempted via Task.pass the dangling
mrb->jmp pointed into mrb_vm_exec's freed stack frame. A subsequent
raise then longjmp'd into that freed frame and crashed.

Restore prev_jmp in the early-return path, matching the normal returns.

  Task.new { Task.pass }
  Task.pass
  raise "boom"          # SIGSEGV before this change

Fixes #6863.
Refs #6864.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-28 13:19:10 +09:00
Yukihiro "Matz" Matsumoto c09196ca36 mruby-task: switch mrb_task_run to mrb_protect_error
mruby/throw.h is documented as a core-internal header that should not
be included from mrbgems or user code, and under MRB_USE_CXX_EXCEPTION
or MRB_USE_CXX_ABI the MRB_TRY/MRB_CATCH macros expand to C++
exception syntax that does not compile in a C source file. The
wrapping added in #6866 (commit ee82a7fcc6) accidentally tripped that
constraint.

Drop the throw.h include and use mrb_protect_error() from
mruby/error.h instead. The helper takes a body function plus
userdata, runs it under its own jmpbuf, and reports whether an
exception was caught. We re-raise via mrb_exc_raise so the visible
behavior matches the previous code: loop_running is cleared on both
success and exception, and an exception propagates back out.

Refs #6866.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-28 12:10:08 +09:00
Yukihiro "Matz" Matsumoto dfc542dbc5 mruby-task: terminate suspended task in suspend test
The "Task#suspend doesn't raise" test left its task parked in
q_suspended_. A later test ("Task.run inside Task.run is a noop")
calls Task.run, and the scheduler will not exit its loop while any
task sits in the suspended (or waiting) queue, so the whole run
hung.

Terminate the task at the end of the suspend test so it does not
leak into the shared scheduler state the next test depends on. This
fixes the hang at its source rather than scrubbing leaked tasks from
the consumer side.

Refs #6866, #6867.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-28 12:02:07 +09:00
Yukihiro "Matz" Matsumoto 20d8e202a2 Merge pull request #6866 from 0x1eef/task_oneloop
task: return `nil` when given a nested call to `Task.run`
2026-05-28 11:28:07 +09:00
0x1eef 4bdde3a0a8 task: add test 2026-05-27 23:27:51 -03:00
0x1eef ee82a7fcc6 fix: wrap mrb_task_run in MRB_TRY/MRB_CATCH 2026-05-27 23:19:43 -03:00
0x1eef 819156e678 task: return nil when given a nested call to Task.run
When you try to start an event loop inside an event loop,
the mruby process will SIGSEGV:

```ruby
Task.new { Task.run }
Task.run
```

This change turns the second call to `Task.run` into a noop
that returns nil instead.

Fix #6865
2026-05-27 23:10:09 -03:00
Yukihiro "Matz" Matsumoto d1289ac8f9 vm.c: defer task switches during gc.iterating
RETURN_IF_TASK_STOPPED used to bail out of mrb_vm_exec as soon as
task.switching was set, even when the exec was called re-entrantly
from inside a heap-walk callback (e.g. ObjectSpace.each_object via
mrb_yield). Bailing in that situation only unwinds the inner exec,
leaving the outer C iteration to keep calling the callback. The
call-info stack drifts on each subsequent mrb_yield, and the program
either trips the cibase assertion in mrb_vm_run or crashes in
__longjmp.

Hold off the switch while mrb->gc.iterating is set so the heap walk
finishes intact; the switch then fires at the next OP boundary once
the walk releases the flag. MRB_TASK_STOPPED is intentionally not
deferred, since exiting promptly is still correct when the task
itself is going away.

Fixes #6862.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-28 07:27:37 +09:00
Yukihiro "Matz" Matsumoto f5a5bfcbf6 limitations.md: replace binding note with general mrbgem pattern
The Kernel#binding entry was treating "feature provided by mrbgem"
as a per-method limitation, which does not scale. mruby implements
much of Ruby's standard surface area through mrbgems, so listing
each one would grow without bound.

Replace it with a single top-level note explaining the general
pattern: which features are available depends on the linked gems,
and a NoMethodError on a familiar Ruby method usually points to a
missing gem rather than a true mruby gap.

Refs #6861.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-27 08:49:07 +09:00
Yukihiro "Matz" Matsumoto 012691279d mruby-string-ext: skip scrub UTF-8 assertions on non-UTF-8 builds
When MRB_UTF8_STRING is undefined, String#scrub is a no-op that
returns the receiver as-is (the #else branch added in ccb62ceb57).
The unit tests assumed UTF-8 semantics unconditionally, so a build
of just mruby-string-ext without UTF-8 strings failed all five scrub
tests.

Guard the UTF-8-dependent assertions with `skip unless "あ".length
== 1`, and add a paired test that asserts the no-op behaviour on the
non-UTF-8 build (skipped on UTF-8 builds).

Verified against the build config from the report:
  - UTF-8 (host-debug): 5 tests pass, 1 skip
  - non-UTF-8 (noutf8):  5 skip, 1 test pass

Closes #6860.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-26 23:10:45 +09:00
Yukihiro "Matz" Matsumoto ccb62ceb57 mruby-string-ext: add String#scrub
Replaces each maximal run of invalid UTF-8 bytes with a replacement
string (U+FFFD by default), returning a valid UTF-8 copy. Mirrors
CRuby's String#scrub (Feature #6752) -- the recovery counterpart to
the existing String#valid_encoding? detection API.

Validation matches utf8code() in src/string.c after the RFC 3629 /
Unicode D93b conformance fixup (#2708): overlong encodings, UTF-16
surrogates, and codepoints above U+10FFFF are all treated as invalid.
This is stricter than the existing mrb_utf8len()-based check used by
valid_encoding?, so a string can report valid_encoding? = true and
still get scrubbed; aligning valid_encoding? is a follow-up.

The block form lives in mrblib on top of two C primitives -- __scrub
and __scrub_chunks -- to avoid VM re-entry from C per CLAUDE.md.
Non-String block return values are coerced via to_s (CRuby raises
TypeError instead; the choice is locked in by test).

Closes #6859.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-25 23:36:42 +09:00
Yukihiro "Matz" Matsumoto 4f398f6126 mruby-task: prefix queue helpers with mrb_task_
`q_insert_task` and `q_delete_task` were exporting bare `q_*` names
from libmruby.a -- single-letter prefixes don't belong to the gem's
namespace and risk colliding with anything else linked in.

Rename to `mrb_task_q_insert` / `mrb_task_q_delete`, matching the
`mrb_task_*` convention already used for the rest of the gem's
externally visible symbols. Callers in task.c and task_queue.c are
updated to the new names.

Closes #6858.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-25 06:48:13 +09:00
Yukihiro "Matz" Matsumoto 19c857a773 mruby-regexp: prefix exposed engine entry points with mrb_re_
The Pike VM and pattern compiler were exporting bare names like
`re_compile`, `re_exec`, `re_free`, `re_is_word_char`, `re_utf8_charlen`,
`re_utf8_decode`. `re_exec` in particular collides with the obsolete
BSD libc function of the same name (still present on FreeBSD/NetBSD
base), so embedding mruby alongside platform regex could surface a
link-time symbol clash.

Rename all six entry points to `mrb_re_*` to keep the gem's external
symbols inside mruby's namespace. Source file names and the public
header path are unchanged.

Refs #6858.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-25 06:09:11 +09:00
Yukihiro "Matz" Matsumoto f6564c7db1 class.c: make define_method_m static
The helper is used only inside class.c (mrb_mod_define_method_m,
mod_define_method, define_singleton_method) -- no need to expose it
through libmruby.a. Drops one of the non-`mrb_*` linkage symbols
flagged in #6858.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-25 06:06:05 +09:00
Yukihiro "Matz" Matsumoto 36dd9eab88 vformat.rb: fix format/arg type mismatch in %!d test case
The test pushed mrb_int via vf.i but the format read int via %!d. On
x86_64/aarch64 the va_arg slots overlap so reading the low 32 bits of
the pushed int64 returned the right value, but on strict-alignment
ABIs (MIPS o32) va_arg(ap, int) reads the alignment padding and the
format prints 0 instead of the value.

Make the format match the helper: vf.i pairs with %!i (reads mrb_int),
matching the surrounding lines 44-50 convention and the "inspect
mrb_int" label.

Reported by vobloeb in #6857.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-24 10:31:14 +09:00
Yukihiro "Matz" Matsumoto b1adab4e87 Merge pull request #6856 from vobloeb/patch-1
test/bintest.rb: tokenize ENV['EMULATOR'] via Shellwords.split
2026-05-23 12:16:39 +09:00
Yukihiro "Matz" Matsumoto d21eceb286 mruby-regexp: disable first-byte skip when pattern can match empty
first_set_walk returned TRUE when it reached RE_MATCH via epsilon
transitions, but that's exactly the case where the optimization is
wrong: an empty-matchable pattern can start matching at any position,
including bytes that aren't in the computed first-byte set. The
skip-ahead loop in pike_vm then advanced past valid empty-match
positions, producing a match at the wrong offset (e.g. /a?/.match("b")
reported the empty match at index 1 instead of 0).

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-23 09:58:31 +09:00
Yukihiro "Matz" Matsumoto b9b8186f00 mruby-regexp: don't bump jump offsets that already point at insertion site
insert_inst was incrementing every offset >= pos, but an offset equal to
pos already points to the new instruction's slot -- bumping it shifts
the target onto whatever code got displaced (typically the body of the
quantified atom). For patterns like /a?b?/ the SPLIT for `a?` then
landed on `CHAR 'b'` instead of the new SPLIT for `b?`, so the "skip a"
thread tried to consume 'b' and died, and both atoms failed to match
zero characters at once.

Fixes #6853.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-23 09:58:30 +09:00
Yukihiro "Matz" Matsumoto cddaec7264 mruby-compiler: bail out of array-literal pattern match opt on splat
The optimization that skips `deconstruct` and the size check when the
case/in value is an array literal trusted node count, ignoring splat.
An element like `*a` expands at runtime, so [*a] was treated as length
1 and matched only patterns of that length.

Fixes #6854.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-23 09:58:30 +09:00
Yukihiro "Matz" Matsumoto 3aea2f7e67 Merge pull request #6855 from mruby/dependabot/github_actions/github-actions-dependencies-1f5b201d0b
build(deps): bump the github-actions-dependencies group with 2 updates
2026-05-23 07:58:14 +09:00
vobloeb c0d8abd0a3 test/bintest.rb: tokenize ENV['EMULATOR'] via Shellwords.split
`Command::CrossTestRunner#emulator` returns a shell-quoted string, which `Build#run_test` un-quotes via `sh`, but `CrossBuild#run_bintest` propagates verbatim through `ENV['EMULATOR']` to `test/bintest.rb`. The latter splices it into an Open3 exec-mode argv, where the literal `"` survives into `execve(2)` and the kernel returns `ENOENT`.

Switching to `Shellwords.split(ENV['EMULATOR'])` round-trips the quoted string correctly and also fixes multi-token emulator commands (e.g. `qemu-aarch64 -L /sysroot`), which currently end up concatenated into `argv[0]`.

Verified against mruby `3.3.0`, `3.4.0`, `4.0.0`, and `master`. Cross-build of mruby for `aarch64-unknown-linux-musl` (qemu-user 8.2.10): bintests went from 17/75 crashing → 100/100 passing.
2026-05-22 17:01:38 +00:00
dependabot[bot] 309b99efa5 build(deps): bump the github-actions-dependencies group with 2 updates
Bumps the github-actions-dependencies group with 2 updates: [github/codeql-action](https://github.com/github/codeql-action) and [j178/prek-action](https://github.com/j178/prek-action).


Updates `github/codeql-action` from 4.35.4 to 4.35.5
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/68bde559dea0fdcac2102bfdf6230c5f70eb485e...9e0d7b8d25671d64c341c19c0152d693099fb5ba)

Updates `j178/prek-action` from 2.0.3 to 2.0.4
- [Release notes](https://github.com/j178/prek-action/releases)
- [Commits](https://github.com/j178/prek-action/compare/6ad80277337ad479fe43bd70701c3f7f8aa74db3...bdca6f102f98e2b4c7029491a53dfd366469e33d)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.35.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions-dependencies
- dependency-name: j178/prek-action
  dependency-version: 2.0.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-22 14:54:27 +00:00
Yukihiro "Matz" Matsumoto 66f438d8fe mruby-bin-debugger: return on OOM in mrb_debug_set_break_method
mrb_debug_set_break_method() freed set_class after mrdb_strdup() of
method_name failed but did not return. Execution continued into
alloc_breakpoint(), which on failure double-freed set_class, or on
success stored the dangling pointer in the breakpoint table for later
use-after-free. Return MRB_DEBUG_NOBUF immediately after the free.

mrdb_strdup uses mrb_malloc_simple which returns NULL on OOM (it does
not raise), so the NULL check is reachable in practice.

close #6851

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-22 07:08:26 +09:00
Yukihiro "Matz" Matsumoto 4507b4a633 mruby-bigint: gate mpz_mod's Barrett path on the algorithm's precondition
Barrett reduction requires x < 2^(2*bits(m)); the gate condition only
required x->sz >= y->sz + 2, which let x.sz reach 25 limbs against a
4-limb modulus. When the precondition is violated, mpz_barrett_reduce
silently truncates high limbs and returns garbage.

Integer#remainder, which routes through mpz_mod, was affected:
(3**500).remainder((2**100)+3) returned the wrong value. Integer#%
took the udiv path via mpz_mmod and was unaffected.

Add x->sz <= 2 * y->sz to the gate so out-of-range inputs fall
through to the general udiv path.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-22 07:08:26 +09:00
Yukihiro "Matz" Matsumoto d30ddd4296 Merge pull request #6852 from mruby/dependabot/pre_commit/pre-commit-hooks-613cf0bdf4
build(deps): bump https://github.com/rubocop/rubocop from v1.86.1 to 1.86.2 in the pre-commit-hooks group
2026-05-22 07:03:27 +09:00
dependabot[bot] a4e12dfc42 build(deps): bump https://github.com/rubocop/rubocop
Bumps the pre-commit-hooks group with 1 update: [https://github.com/rubocop/rubocop](https://github.com/rubocop/rubocop).


Updates `https://github.com/rubocop/rubocop` from v1.86.1 to 1.86.2
- [Release notes](https://github.com/rubocop/rubocop/releases)
- [Changelog](https://github.com/rubocop/rubocop/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rubocop/rubocop/compare/v1.86.1...v1.86.2)

---
updated-dependencies:
- dependency-name: https://github.com/rubocop/rubocop
  dependency-version: 1.86.2
  dependency-type: direct:production
  dependency-group: pre-commit-hooks
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-21 16:28:35 +00:00
Yukihiro "Matz" Matsumoto 206b9f7477 mruby-bigint: pre-reduce base in mpz_powm_montgomery to satisfy REDC
REDC's input T must satisfy T < R*N; mpz_powm_montgomery() computed
T = base * R^2 without first reducing base modulo n. When base >= n,
T exceeds R*N and REDC silently truncates upper limbs, producing a
wrong base_mont and ultimately a wrong result.

Use mpz_mmod (general division path) to pre-reduce base mod n before
the multiplication by R^2.

Visible effect: (2**160).pow(2, (2**40)+1) returned 0 instead of 1.
A separate pre-existing issue in mpz_mod's Barrett path (broken
precondition check) means that path could return a wrong reduction
for large operands; mmod sidesteps that path entirely.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-21 17:07:51 +09:00
Yukihiro "Matz" Matsumoto 6ac2c3dc56 mruby-bigint: maintain canonical sn=0 when trim reduces sz to 0
mpz_t treats sn (sign) as the canonical "is zero" flag (zero_p(x) :=
(x)->sn == 0). When an arithmetic operation produces a value whose
limbs trim to zero size, sn must be reset to 0 to preserve the
invariant. Several call sites already enforced this locally (e.g.
mpz_sub line 616); make trim() responsible so every caller benefits.

Without this, an inconsistent zero bignum (sn!=0, sz=0) can flow into
mpz_sqr, miss the zero_p guard, and reach mpz_init_heap with hint=0
where mpn_zero(NULL, 0) invokes UB (memset() declares its first
argument nonnull). The trip survives at runtime on glibc but is
formally undefined behavior, flagged by UBSan via Integer#pow(b,e,m)
with specific operands.

close #6849

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-21 17:04:08 +09:00
Yukihiro "Matz" Matsumoto 72c117cb9e variable.c: guard assign_class_name against unresolvable symbol
mrb_sym_name_len() returns NULL when sym is 0, out of range, or
references a freed symbol slot. assign_class_name() indexed [0]
without checking, so malformed bytecode whose OP_CLASS operand
indexed past irep->slen could feed a bogus sym here and crash on
NULL[0]. Skip the class-naming side effect when the sym does not
resolve to a name.

close #6842

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-21 16:11:48 +09:00
Yukihiro "Matz" Matsumoto 465e634d48 mruby-regexp: fix SEGV on uninitialized Regexp's hash and ==
regexp_init() called re_compile() before setting @source / @flags
IVs, so a Regexp that survived a compile-time exception (e.g. picked
up via ObjectSpace.each_object after `Regexp.new("(")` raised) was
left with no @source. obj.hash then dereferenced nil through
mrb_str_hash() and crashed.

Set the IVs before re_compile(), and make regexp_hash / regexp_eql
defensive against a non-String @source so Regexp.allocate.hash also
behaves.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-21 12:32:40 +09:00
Yukihiro "Matz" Matsumoto 9a1c1eabf9 Merge pull request #6841 from dearblue/gc
Small refactor for `incremental_sweep_phase()`
2026-05-21 10:09:55 +09:00
Yukihiro "Matz" Matsumoto 54c8427df2 mruby-regexp: handle \b inside character class as backspace
Inside `[...]`, `\b` denotes U+0008 (backspace) -- the same as
MRI/Onigmo and PCRE.  parse_escape() was missing the case, so
the backslash was dropped and the bare letter `b` was inserted
into the class.  `[\b]` therefore matched every `b` instead of
backspace.

Add `case 'b': return '\b';` to parse_escape().  The function
is only reached from the character-class body and range
endpoints; the top-level dispatcher emits RE_WBOUND for `\b`
before falling through, so the word-boundary semantics outside
`[...]` are unchanged.

Reported by Sam Ruby in matz/spinel#632; same engine bug
affects both spinel and mruby.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-21 08:23:15 +09:00
dearblue df38160d82 Omit the check whether the typetag is MRB_TT_FREE after calling obj_free()
With commit cbc3dbedb4, freed objects will always be `MRB_TT_FREE`.
2026-05-20 21:15:54 +09:00
dearblue 2c0bccab66 Use else to skip the while loop in incremental_sweep_phase()
As a result, the variables can also be localized.
2026-05-20 21:15:54 +09:00
Yukihiro "Matz" Matsumoto 17d124b00d array.c (mrb_ary_splice): re-modify a after self-aset ary_dup
`a[range] = a` on a long-enough array tripped a heap-buffer-overflow
in value_move(). mrb_ary_splice's self-aset branch calls ary_dup(a)
to get an independent copy of the source elements, but ary_dup ->
ary_replace converts the source to shared as a copy-on-write
optimization when the length exceeds ARY_REPLACE_SHARED_MIN. After
that, a->as.heap.aux is reinterpreted as `shared` (the union member)
and ARY_CAPA(a) reads from the shared pointer's bits rather than
the real capacity. The expand-capa check below then silently mis-
sizes and value_move walks past the buffer.

Re-modify `a` immediately after ary_dup to un-share before the in-
place mutation. The buffer reads through `argv` (which now points
into the dup's storage) stay valid because ary_modify on a multi-
reference shared array allocates a fresh buffer for `a` and leaves
the original buffer owned by the dup.

Found via clusterfuzz mruby_fuzzer testcase 6525563811725312;
regression test covers a[3, 2] = a on a 31-element array (above
the ARY_REPLACE_SHARED_MIN=20 threshold).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 14:27:22 +09:00
Yukihiro "Matz" Matsumoto 3e2ce88eab hw-spi: wrap splat-bearing method signatures as code in README
After f91936b06e kept the splat notation unescaped, prettier
kept flagging the standalone `*` in the SPI#write / SPI#transfer
headings every CI run.  Wrap the two signatures in backticks so
prettier treats them as inline code (which they are), and the
`*data` form stays unescaped without further conflict.  Same
convention is already used in mruby-kernel-ext/README.md
(e.g. `### \`fail(*args)\``).

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-20 08:35:22 +09:00
Yukihiro "Matz" Matsumoto 7378be58cb Merge pull request #6838 from dearblue/gc_stat
Use `MRB_SYM()` in `gc_stat()`
2026-05-19 22:44:41 +09:00
Yukihiro "Matz" Matsumoto ebe4a7aed0 Merge pull request #6837 from dearblue/mrb_const_cache_clear
Avoid surrounding `#if` when using `mrb_const_cache_clear()`
2026-05-19 22:44:37 +09:00
dearblue 6d12f1f331 Use MRB_SYM() in gc_stat() 2026-05-19 22:28:15 +09:00
dearblue 6daa33c9a8 Avoid surrounding #if when using mrb_const_cache_clear()
`mrb_const_cache_clear()` is always available.
2026-05-19 21:22:27 +09:00
Yukihiro "Matz" Matsumoto ae29ab7db9 numeric.c: bounds-check float in Float#div before mrb_int cast
`Float#div(Integer)` cast its receiver to mrb_int unconditionally,
which is undefined behavior when the float is outside the
representable mrb_int range.  ASan reports the UB on inputs like
`5e+56.div(1)`.

Reported by ClusterFuzz testcase
clusterfuzz-testcase-minimized-mruby_fuzzer-5137605569347584.

Guard the cast with FIXABLE_FLOAT and route over-range receivers
to mrb_bint_div when MRB_USE_BIGINT is defined (matching
flo_rounding_int's pattern), or raise via mrb_int_overflow when
not.  Existing `(float in range).div(int)` semantics are
unchanged.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-19 12:15:38 +09:00
Yukihiro "Matz" Matsumoto 73255d3b70 mruby-numeric-ext: avoid signed overflow on Integer#gcd/lcm with MRB_INT_MIN
Negating MRB_INT_MIN (-2^63) is signed overflow (UB) because
2^63 does not fit in mrb_int.  Both `mrb_int_gcd` and `int_lcm`
took the absolute value via `if (x < 0) x = -x`, which trips on
MRB_INT_MIN.

Reported by ClusterFuzz testcase
clusterfuzz-testcase-minimized-mruby_fuzzer-5137605569347584.

* mrb_int_gcd: cast each input to mrb_uint before negating; the
  Euclidean reduction runs in unsigned.  The cast back at the
  end yields MRB_INT_MIN only when the mathematical gcd is 2^63
  (i.e., gcd(MIN, 0) or gcd(MIN, MIN)).
* int_gcd: detect the negative return value from mrb_int_gcd
  and raise via mrb_int_overflow, since the true result does
  not fit.
* int_lcm: short-circuit raise when either operand is
  MRB_INT_MIN (after the existing zero check), since the abs
  would overflow and the lcm with any non-zero operand could
  not fit anyway.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-19 12:05:33 +09:00
Yukihiro "Matz" Matsumoto db2845aae0 mruby-regexp: bounds-check group index in RE_BACKREF
The RE_BACKREF execution path read `captures[group * 2]` and
`captures[group * 2 + 1]` without verifying that the group
index fit in the allocated captures array. A pattern like
`/\1/` (no capture group, but a backreference to group 1) is
accepted by the compiler and lands in execution with `ncap = 2`
(only group 0 slots) and an instruction asking for group 1 --
a 4-byte read past the end of the allocation.

Reported by ClusterFuzz testcase
clusterfuzz-testcase-minimized-mruby_fuzzer-5474946829844480.

Add `if (group * 2 + 1 >= ncap) return FALSE;` ahead of the
captures access, mirroring the bounds guard already present in
RE_SAVE. The compiler's permissive `\<digit>` handling stays
unchanged; the runtime now treats a reference to a non-existent
group as a non-match rather than UB.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-19 10:40:45 +09:00
Yukihiro "Matz" Matsumoto 5bb4a15086 mruby-regexp: cap bt_match recursion depth
The backtracking engine recurses via C function calls at RE_SPLIT,
RE_SPLITNG, RE_SAVE, RE_LOOKAHEAD, RE_NEG_LOOKAHEAD, RE_LOOKBEHIND,
and RE_NEG_LOOKBEHIND.  Patterns like `(?=)+` make the engine
recurse without consuming input, exhausting the C stack and
triggering SIGSEGV long before MRB_REGEXP_STEP_LIMIT is reached
(each recursion charges only ~1 step, but each frame costs ~150
bytes of stack).

Reported by ClusterFuzz testcase
clusterfuzz-testcase-minimized-mruby_fuzzer-4653331195953152.

Add an integer recursion-depth counter passed alongside the step
counter, and abort the current branch with FALSE when it exceeds
MRB_REGEXP_RECURSION_LIMIT (default 1000, configurable like
STEP_LIMIT).  Legitimate patterns nest only a few levels;
pathological inputs bail without crashing the VM.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-19 10:34:39 +09:00
Yukihiro "Matz" Matsumoto 3e676af568 Merge pull request #6836 from hasumikin/fix/task
Fix mruby-task: keep join waiter wakeup under one IRQ critical section
2026-05-18 16:26:31 +09:00
HASUMI Hitoshi c4d205496d Fix mruby-task: keep join waiter wakeup under one IRQ critical section 2026-05-18 16:11:04 +09:00
Yukihiro "Matz" Matsumoto 1317a8bf5d Merge pull request #6835 from hasumikin/task-queue
Introduce Task::Queue
2026-05-18 15:38:16 +09:00
HASUMI Hitoshi a1a77e5acc Call task_check_scheduler_lock in queue_pop_try to avoid dead lock 2026-05-18 15:30:44 +09:00
HASUMI Hitoshi c9ae2524ee Cache Task::Error in task_error_class_ iniailized in mrb_init_task_queue() 2026-05-18 15:26:38 +09:00
HASUMI Hitoshi 458078f10e Introduce Task::Queue
- This patch implements `Task::Queue` mirroring `Thread::Queue` in CRuby.
- Producer/Consumer pattern is now possible with no polling

## No Top-level pollution
- No top-level `Queue` defined
- No `TaskError`. Instead, `Task::Error` happens when `Task::Queue#pop(true)` when empty
- No `ClosedQueueError`. `Task::Error` also happens when pushing to closed queue

## Future Work
- `Task::SizedQueue`
2026-05-18 14:23:42 +09:00
Yukihiro "Matz" Matsumoto d1bffb902e Merge pull request #6834 from Asmod4n/glib_hal
mruby-task GLib HAL
2026-05-17 23:31:33 +09:00
Asmod4n 17858cc5bd mruby-task GLib HAL
Lets mruby-task embed cleanly in any GLib-based event loop -- GTK,
libsoup, GStreamer, or anything else built on GMainContext. Tasks
become regular GSources, so the scheduler runs alongside whatever
else is on the loop without polling or busy-waiting.

Sleeping tasks cost zero CPU: the HAL parks until the next wakeup
deadline rather than ticking on a fixed cadence. Multiple mrb_states
on the same thread share one dispatcher and one ticker. Preemption,
Task.run, sleeper wakes, and foreign-loop integration all use the
same primitives, so embedders can mix Task.run with g_main_loop_run
freely.

ref mruby#6825
2026-05-17 13:44:20 +02:00
Yukihiro "Matz" Matsumoto aa25be69d7 Merge pull request #6833 from Asmod4n/patch-4
Improve wakeup tick condition check
2026-05-17 19:02:42 +09:00
Yukihiro "Matz" Matsumoto c8dd299ca8 mrbgems.md: document Platform Ports and External HAL Providers
Add a section on the `ports/<name>/` mechanism (`conf.ports`
selection, fallback chain) and on the external HAL provider
naming convention (`hal-<short>-<conf>` overrides the bundled
ports/* of the gem whose name ends in `-<short>`).

Also add `ports/<name>/` to the GEM structure tree.

ref #6825

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-17 18:48:23 +09:00
Yukihiro "Matz" Matsumoto e7f5387f5d gem.rb: detect external HAL by hal-<short>-* naming
67f137e201 introduced spec.hal_pattern on the target gem to
declare its expected HAL provider names.  But the decision to
override is naturally an opt-in choice belonging to the external
HAL gem (or build_config), not the target.  Drop hal_pattern and
hardcode the naming rule in resolve_external_hal!: a gem named
`hal-<short>-<conf>` overrides the gem whose last `-`-separated
segment is <short>.  No spec attribute on the target gem; no flag
on add_dependency; the convention does all the work.

The pre-be6413f0d8 convention was `hal-<platform>-<gem>` (e.g.,
hal-posix-task) for platform sort across gems.  Now that the
common platforms live under ports/, sorting by which target gem
a HAL is for is more useful, so the order is reversed:
`hal-<gem>-<conf>` (e.g., hal-task-glib).

ref #6825

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-17 18:38:38 +09:00
Yukihiro "Matz" Matsumoto 67f137e201 gem.rb: hal_pattern for external HAL provider override
A gem can declare

  spec.hal_pattern = /\Ahal-.*-task\z/

to indicate that another gem whose name matches the pattern (and
which depends on this gem for headers) replaces the built-in
ports/<conf.ports>/ HAL implementation.  After all gems are set
up, List#resolve_external_hal! drops the target's ports/* objs
from its object list so the matching gem supplies the HAL
symbols.  Two or more matches is reported as a build error.

This restores the pre-be6413f0d8 ability to maintain an
out-of-tree HAL via add_dependency + naming convention, without
reintroducing the "HAL information scattered across gems"
problem: the parent gem still owns the scheduler, headers, and
bundled posix/win ports; external HAL gems are an explicit,
opt-in override.

Declare /\Ahal-.*-task\z/ for mruby-task -- the same naming
pattern used before be6413f0d8.

ref #6825

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-17 17:33:26 +09:00
Hendrik 8b63ddbfd1 Refactor task wakeup logic and add comments
Enhance wakeup logic for sleeping tasks to handle UINT32_MAX sentinel case and prevent race conditions with mrb_tick.

This enables tickless Task hals to be build.
2026-05-17 08:14:58 +02:00
Hendrik 2668619143 Improve wakeup tick condition check
We got a off by one error here, checking against UINT32_MAX doesn't silently set a wrong value.

When running the task mgem with a busy loop this doesn't cause an issue, but with a tickless timer only one timer ever gets fired and then the task mgem stops working.
2026-05-17 07:31:08 +02:00
Yukihiro "Matz" Matsumoto f22a991065 build: pick first matching ports/<name>/ per gem
When `conf.ports` was given multiple names, every matching ports/
directory for each gem was compiled -- fine for single-name builds
and for gems that have at most one matching port, but a footgun
once HAL-using gems start providing multiple alternative ports
(e.g. mruby-task with both posix and an alternative runloop).

Iterate the chain and break on the first match.  Existing builds
that pass at most one name per gem keep the same behavior;
`conf.ports :rp2040, :posix` now reads as "use rp2040 if a gem has
that port, otherwise posix" rather than "compile both".

ref #6825

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-17 06:57:54 +09:00
Yukihiro "Matz" Matsumoto 40264c9aad mruby-compiler: accept double-quoted strings in case/in patterns
The p_value rule only accepted bare tSTRING tokens, which the lexer
emits for single-quoted strings.  Double-quoted strings emit
tSTRING_BEG ... tSTRING (or with interpolation, tSTRING_BEG
string_rep tSTRING), so

  case "hello"
  in "hello"
    :match
  end

raised "syntax error, unexpected string literal" at the `"` after
`in`.  Use the existing `string` non-terminal instead of bare
tSTRING, which also enables alternation (`"a" | "b"`), interpolation
(`"hel#{x}"`), and concatenation by juxtaposition in patterns.

close #6830

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-16 22:01:52 +09:00
Yukihiro "Matz" Matsumoto b059f3df25 mruby-bin-mrbc: skip multi-input debug-info bintest when bin-mruby absent
The bintest added in 0d87198c92 runs the compiled output via the
mruby binary and uses Kernel#puts in the script body.  In builds
that omit mruby-bin-mruby, the test failed with "sh: bin/mruby:
not found" before reaching the actual assertion.

Skip the assert block when bin/mruby isn't built, and switch the
script's puts to print since Kernel#puts only exists when mruby-io
is loaded (same pattern as #6814).

close #6831

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-16 21:17:41 +09:00
Yukihiro "Matz" Matsumoto e5c2479ee6 mruby-bin-mruby: skip regexp split bintest when mruby-regexp absent
The bintest added in 9d8d41006b uses `"hello world".split(/\s+/)`,
which raises `uninitialized constant Regexp` in builds that omit
mruby-regexp. Probe whether the mruby binary defines Regexp and
skip the whole assert block when it doesn't; the test is regression
coverage for mruby-regexp's String#split override, so when regexp
isn't loaded there is nothing to verify.

close #6832

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-16 20:23:23 +09:00
Yukihiro "Matz" Matsumoto 087da64f99 Merge pull request #6829 from Asmod4n/master
add IO#autoclose= and IO#autoclose?
2026-05-14 17:04:36 +09:00
Hendrik 9eb8865a8a Fix documentation for autoclose? method 2026-05-14 09:57:26 +02:00
Asmod4n aea2bbcbe5 add IO#autoclose= and IO#autoclose? 2026-05-14 09:53:18 +02:00
Yukihiro "Matz" Matsumoto 3e5e84e391 mruby-task: update README and headers for ports-based HAL layout
The HAL was integrated into mruby-task/ports/{posix,win}/ in
be6413f0d8 and the function names were updated in 610ff67906, but
the docs and one header still described the old separate-gem layout:

  - mrbgems/mruby-task/README.md described hal-posix-task and
    hal-win-task as separate gems, used the pre-rename function
    names (mrb_task_hal_*), and omitted mrb_hal_task_sleep_us.
    Rewrote the HAL section to match the current ports/ model.

  - mrbgems/mruby-task/include/task.h had three orphan declarations
    (mrb_task_hal_init / _final / _idle_cpu) from before the
    rename. Removed; the real declarations are in task_hal.h.

  - mrbgems/mruby-task/include/task_hal.h had a comment referring
    to the removed hal-* gems.

  - doc/guides/amalgamation.md listed hal-posix-io and hal-posix-task
    as platform-specific gems alongside mruby-io and mruby-task; both
    are now ports under the parent gem.

Reported by Asmod4n in #6825.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-14 13:48:55 +09:00
Yukihiro "Matz" Matsumoto d1608ba72e mruby.h: add MRB_API to mrb_const_cache_clear
The function is the symmetric counterpart of mrb_method_cache_clear
declared two lines above, but was added without MRB_API in
50bc8c6136. Both are VM-internal cache invalidators exposed in the
public header under the same #ifndef MRB_NO_*_CACHE pattern; make
their decoration consistent.

Reported by dearblue in #6826.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-14 13:23:05 +09:00
Yukihiro "Matz" Matsumoto 6d2d31ac5f mruby.h: use MRB_INLINE for mrb_funcall_argv1 / mrb_funcall_argv2
These public convenience wrappers were declared with raw
`static inline`, while every other public inline helper in mruby.h
uses MRB_INLINE. MRB_INLINE expands to static inline, so this is a
spelling-only change for grep-consistency across the C API.

Reported by dearblue in #6827.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-14 13:12:26 +09:00
Yukihiro "Matz" Matsumoto 9e93337479 mruby-regexp: copy named-capture names into owned arena
re_compile stored raw pointers into the pattern source in
pat->named_captures[i].name. Two ways this could dangle:

  - With /x, the source was c.stripped, freed at end of compile.
    Later reads (regexp construction, MatchData[:name] lookup) hit
    freed memory.

  - Without /x, the pointer aliased the input string's RSTRING_PTR.
    Mutating that string after Regexp.new could re-buffer it, leaving
    name dangling.

Allocate one arena buffer per regexp (only when num_named > 0) and
copy all names in. Common-case regexps without named captures pay
zero bytes.

Reported by OSS-Fuzz (testcase 5695283416858624).

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-13 12:34:50 +09:00
Yukihiro "Matz" Matsumoto 403b75fbeb fp_uscale.c: clamp parser underflow guard to POW10_MIN
mrb_read_float's underflow short-circuit used `final_p < -342 - nd`,
which for nd > 1 can be below POW10_MIN (-343). That let
parse_decimal call prescale() with a final_p below POW10_MIN, causing
an out-of-bounds read of pow10_tab. Tighten the guard to
`final_p < POW10_MIN`; values below that threshold cannot be
represented as a non-zero double for any mantissa within the parser's
19-digit cap.

Reported by OSS-Fuzz (testcase 6097379597287424).

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-13 12:02:30 +09:00
Yukihiro "Matz" Matsumoto 27e14c16c4 limitations.md: document Class#initialize re-invocation difference
mruby's Class#initialize accepts re-invocation through __send__
silently, while CRuby raises TypeError. The superclass argument is
ignored on re-invocation, so no destructive side effect is possible.
Reported on Twitter by cacao_soft.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-13 12:02:30 +09:00
Yukihiro "Matz" Matsumoto 82de0b0ee5 Merge pull request #6824 from mruby/dependabot/github_actions/github-actions-dependencies-2cbc4c3382
build(deps): bump actions/labeler from 6.0.1 to 6.1.0 in the github-actions-dependencies group
2026-05-13 07:40:09 +09:00
dependabot[bot] 0eaae2824b build(deps): bump actions/labeler
Bumps the github-actions-dependencies group with 1 update: [actions/labeler](https://github.com/actions/labeler).


Updates `actions/labeler` from 6.0.1 to 6.1.0
- [Release notes](https://github.com/actions/labeler/releases)
- [Commits](https://github.com/actions/labeler/compare/634933edcd8ababfe52f92936142cc22ac488b1b...f27b608878404679385c85cfa523b85ccb86e213)

---
updated-dependencies:
- dependency-name: actions/labeler
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-12 20:37:41 +00:00
Yukihiro "Matz" Matsumoto 478ada3bf4 fp_uscale.c: clamp precision in %g to avoid OOB in fixed_width
When sprintf is called with a precision larger than the double's
significand width (e.g. "%.51g"), fixed_width() indexed pow10 tables
out of bounds and produced a negative shift exponent. Cap the
internal digit count to 18 in the %g branch, matching the existing
%e and %f branches; downstream loops already zero-pad to the
caller's precision so visible output is unchanged.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-12 11:19:20 +09:00
Yukihiro "Matz" Matsumoto 16151a0daa proc.c: mark Proc#dup / Proc#clone copies as orphan blocks
A copied Proc now always carries `MRB_PROC_ORPHAN`, so calling a
`dup`'d block that contains `break` or `return` raises
`LocalJumpError` even while the original yielding method is still on
the stack.

This is stricter than CRuby — which only marks the copy orphan once
the original yielding method returns — but matches mruby's
memory-first design: tracking the original via a back pointer in
RProc would also enlarge the GC mark set. dearblue's option (1) in
the linked issue, accepted for the simpler RProc layout.

Document the divergence in `doc/limitations.md` and add a regression
test in `test/t/proc.rb`.

close #6345

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-11 15:06:24 +09:00
Yukihiro "Matz" Matsumoto 322642364a capi.md: document that dfree handlers must not re-enter the VM
The `dfree` callback registered via `mrb_data_type.dfree` runs from
inside GC sweep, so allocating Ruby objects, calling `mrb_funcall` /
`mrb_yield`, raising exceptions, or otherwise re-entering the VM
can trigger a recursive GC that revisits the same object and causes
double-free (see #6316). Make the rule explicit in the "Wrapping C
Structures" section.

close #6316

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-11 14:39:01 +09:00
Yukihiro "Matz" Matsumoto 3fc7dd1858 mruby-proc-ext: Proc#curry should check max arity for lambdas
`lambda {|a, b=nil|}.curry(3)` used to silently return a curried Proc;
CRuby raises `ArgumentError` because the lambda accepts at most 2 args.
Use `self.parameters` to compute the upper bound (count `:req`/`:opt`
entries, unbounded if `:rest`/`:keyrest` is present) and add the
corresponding range check alongside the existing minimum check.

close #2855

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-11 14:06:28 +09:00
Yukihiro "Matz" Matsumoto 46151db893 Merge pull request #6823 from jbampton/pin-all-actions
Pin remaining actions to hash
2026-05-11 13:07:12 +09:00
Yukihiro "Matz" Matsumoto 9d8d41006b mruby-regexp: alias original String#split before overriding it
`class String; def split` in mruby-regexp/mrblib/string_regexp.rb
replaced the C-defined String#split rather than overriding it, so
the in-Ruby `return super if pattern.nil?` paths raised
NoMethodError for any `"x".split(...)` call once mruby-regexp was
loaded (the default full-core production binary).

The regression wasn't caught by the test suite because per-gem
tests run under mrb_open_core() with only the gem's dep_list, so
the broken override is never visible from test/t/string.rb
(mruby-test, not mruby-regexp).

Add `alias __split split` at the top of the override class body,
which captures the C-defined method, and change the Ruby override
to delegate via `__split(pattern, limit)` for non-regexp fallback
paths. Add a bintest under mruby-bin-mruby that runs through
bin/mruby (full gem load) to catch this regression class.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-11 12:45:43 +09:00
John Bampton 788ad33333 Pin remaining actions to hash 2026-05-11 13:21:28 +10:00
Yukihiro "Matz" Matsumoto 0a78ab986d Merge pull request #6822 from jbampton/remove-trailing-whitespace
Run prek; remove trailing whitespace; cleanup markdown
2026-05-11 10:56:12 +09:00
Yukihiro "Matz" Matsumoto f91936b06e hw-spi: keep splat parameter notation unescaped in README
The previous commit's prettier pass escaped `*data` to `\*data` in
the SPI#write / SPI#transfer method signatures. The escape is
defensive (prettier avoids any standalone `*` to dodge italic
misparses) but unnecessary in these positions: GFM only treats `*`
as italic when paired with a closing `*`, which never happens in a
method signature. The unescaped form reads more naturally to Ruby
users.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-11 10:56:00 +09:00
Yukihiro "Matz" Matsumoto 0d87198c92 mruby-compiler: preserve line number across multi-file boundary
When mrbc compiles multiple input files (e.g. `mrbc -g -o out.mrb
a.rb b.rb`), the bison parser's one-token lookahead can buffer the
final token of one file before partial_hook switches to the next.
By the time bison reduces that token into an AST node,
`mrb_parser_set_filename` has already reset `p->lineno` to 0, so
init_var_header recorded lineno=0 for the previous file's last
statement and codegen propagated the previous instruction's line.

Save the lineno into `prev_file_lineno` immediately before the
reset so init_var_header can restore the correct value when it
detects the lookahead edge case (lineno==0 && filename_index>0).

close #1316

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-11 10:38:02 +09:00
Yukihiro "Matz" Matsumoto 3dbf5f6e27 build/command.rb: route mrbc output through tempfile to avoid pipe race
The previous IO.popen capture of mrbc stdout (`-o-`) is vulnerable
to a Windows MinGW race: with parallel rake (-m), stdout pipe
inheritance can allow unrelated `_pp` build-progress messages from
sibling worker threads to leak into the captured pipe content,
corrupting the generated C file (recently seen as `CC build/...`
text inside gem_test.c, causing compile errors at unrelated lines).

Switch to `mrbc -o <tmpfile>` so the output is materialised in a
file before merging into the destination, fully isolating mrbc
from the parent's STDOUT.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-11 09:52:35 +09:00
Yukihiro "Matz" Matsumoto ad0ea8571f fp_uscale.c: zero-initialize digs to silence MinGW gcc warning
MinGW gcc emits -Wmaybe-uninitialized for `digs[0]` in the
`mrb_format_float` function because it cannot prove that
`count_digits()` always returns >= 1. The code is correct
(count_digits's `d == 0` early return makes the lower bound
clear to a human reader), but gcc's flow analysis does not
follow through. Initialize `digs` to silence the false positive
across all MinGW gcc CI configurations.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-11 08:47:05 +09:00
Yukihiro "Matz" Matsumoto 9bb40386ce limitations.md: document nested def scope in singleton methods
mruby places `def` written inside `def self.foo` on the receiver's
singleton class (making it a class method of the enclosing class).
CRuby places it as an instance method of the lexical enclosing
class. This is a long-standing divergence that we've chosen to
document rather than change.

close #1536

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-11 08:31:19 +09:00
Yukihiro "Matz" Matsumoto 89e81e9130 limitations.md: document absence of implicit type conversion
Add a section explaining that mruby intentionally does not consult
to_int/to_str/to_ary/to_hash for implicit type coercion in built-in
operations, even though identity versions remain defined on the
corresponding built-in types. Note that Float#to_int and Array#to_ary
are not defined, and contrast with explicit conversion methods
(to_i, to_s, to_a) which do work.

ref #2979

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-11 08:13:51 +09:00
Yukihiro "Matz" Matsumoto fe17d66363 mruby-string-ext: reject surrogates and ill-formed UTF-8
Make `Integer#chr("UTF-8")` reject UTF-16 surrogate code points
(U+D800..U+DFFF), and make `String#ord` reject ill-formed UTF-8 byte
sequences (overlong encodings, surrogates encoded as UTF-8, and code
points above U+10FFFF), matching CRuby and RFC 3629.

The `utf8code()` helper now decodes the code point first and then
validates the range per byte length:
  len=2: cp >= 0x80                 (rejects overlong)
  len=3: cp >= 0x800 and not D800..DFFF
  len=4: 0x10000 <= cp <= 0x10FFFF

close #2708

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-11 07:48:23 +09:00
John Bampton bda1d2d5a5 Run prek; remove trailing whitespace; cleanup markdown 2026-05-10 14:56:53 +10:00
Yukihiro "Matz" Matsumoto db29955b7a mruby-internal: switch internal mrb_funcall_id callers to argv1/argv2
Replace the 12 in-tree mrb_funcall_id call sites (all argc=1 or
argc=2) with the typed inline helpers added in the previous commit.
After inlining each call site allocates exactly the slots it needs
on its own frame instead of going through mrb_funcall_id's fixed
16-element argv buffer.

ref #5804

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-10 01:32:33 +09:00
Yukihiro "Matz" Matsumoto 575609f3b9 mruby.h: add mrb_funcall_argv1/argv2 inline helpers
mrb_funcall_id() always reserves a 16-element argv buffer on its
stack frame regardless of the actual argument count. The two new
static inline wrappers allocate exactly one or two argument slots,
saving 100-220 bytes of stack per call after inlining (the savings
depend on mrb_value size under the active boxing configuration).

close #5804

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-10 01:30:29 +09:00
Yukihiro "Matz" Matsumoto b4f6450509 mruby-compiler: do not flip + and - opcode for negative literal
The peephole in gen_addsub() rewrote `q + -n` into OP_SUBI n (and
`q - -n` into OP_ADDI n) by negating n. For numeric receivers this is
equivalent, but for receivers overriding + or - the runtime fallback
dispatches the flipped method, losing the original operator. Restrict
the fold to non-negative immediates; negative falls through to the
normal OP_ADD/OP_SUB path with LOADI of the literal value.

close #2557
ref #2579

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-09 19:47:27 +09:00
dearblue 2e429a034d gem.rb: add post_user_config hook for gem-author finalization
Lets a gem author register a block that runs after the user's
`build.gem` block has been processed.  Intended for filling in
defaults that depend on user-supplied configuration — for example,
library auto-detection that should only kick in when the user
hasn't explicitly chosen which library to use.

Initialization order is now:
  1. block in MRuby::Gem::Specification.new (gem author defaults)
  2. block in build.gem (user override)
  3. block in post_user_config (gem author finalize, new)

Closes #6212, picked from PR by dearblue with the method renamed
from last_initializer to post_user_config (per matz's review).

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-09 15:10:58 +09:00
Yukihiro "Matz" Matsumoto 81cfeb917e Merge pull request #2479 from take-cheeze/sym_slice
Implement `Symbol#slice` and `Symbol#[]` in mruby-symbol-ext.
2026-05-09 10:32:20 +09:00
Yukihiro "Matz" Matsumoto 43c0b50cbd mruby-socket: implement Socket.ip_address_list
Returns an Array of Addrinfo objects for all local IP addresses
(IPv4 and IPv6) on every network interface, matching CRuby's API.

Backed by getifaddrs(3) on POSIX and GetAdaptersAddresses on
Windows (requires iphlpapi.lib, added to the Windows linker libs).
The HAL returns binary sockaddr strings; src/socket.c wraps each
into Addrinfo so the wrapping code stays platform-agnostic.

Resolves the second error reported in #5659 (after the IO.select
fix from the HAL split): "undefined method 'ip_address_list' for
Class". Issue #5659 itself is closed; this lands the missing API.

Ref #5659.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-09 10:22:19 +09:00
Yukihiro "Matz" Matsumoto 7d0d6fd413 Merge pull request #6821 from mruby/dependabot/github_actions/github-actions-dependencies-937d73b4db
build(deps): bump github/codeql-action from 4.35.2 to 4.35.3 in the github-actions-dependencies group
2026-05-09 08:09:06 +09:00
dependabot[bot] 8703a49fb4 build(deps): bump github/codeql-action
Bumps the github-actions-dependencies group with 1 update: [github/codeql-action](https://github.com/github/codeql-action).


Updates `github/codeql-action` from 4.35.2 to 4.35.3
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v4.35.2...v4.35.3)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.35.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-08 14:54:16 +00:00
Yukihiro "Matz" Matsumoto c0fb9a190c host-shared.rb: produce libmruby.so for distro packaging
Builds the shared library FROM the existing static archive via
-Wl,--whole-archive, leaving the static-build pipeline (and the
test infrastructure that depends on it) untouched.

Produces in build/host/lib/:
  libmruby.so              SONAME=libmruby.so.<MAJOR>.<MINOR>
  libmruby.so.<MAJOR>.<MINOR>  symlink for runtime DT_NEEDED resolution
  libmruby.map             linker version script: MRUBY_<RELEASE_NO>
  libmruby_core.so + symlink (similarly)

Symbol versioning ties to MRUBY_RELEASE_NO (e.g. MRUBY_40000 for
4.0.0). mruby has historically had ABI breaks at TEENY granularity,
so the version tag uses the full release number rather than just
MAJOR.MINOR.

Closes #6239.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-08 20:48:58 +09:00
Yukihiro "Matz" Matsumoto e95c0fc683 Merge pull request #6691 from jbampton/add-pre-commit-hook
pre-commit: add hook to ensure Makefiles are indented with tabs
2026-05-08 19:31:54 +09:00
Yukihiro "Matz" Matsumoto 274af06d06 Merge pull request #6596 from jbampton/add-hook-check-useless-excludes
pre-commit add official meta hook `check-useless-excludes`
2026-05-08 19:31:51 +09:00
Yukihiro "Matz" Matsumoto c8c8522519 Merge pull request #6218 from dearblue/init-functions
Reduced description of `mrb_init_core()`
2026-05-08 19:21:47 +09:00
Yukihiro "Matz" Matsumoto 61a5e290b3 Merge pull request #6334 from dearblue/each_objects
Remove `iterating` variable from `mrb_objspace_each_objects()`
2026-05-08 12:01:49 +09:00
dearblue cf20687ee7 mruby-eval: unify f_instance_eval and f_class_eval
Their bodies were nearly identical: same argument parsing,
same proc creation, same target-class plumbing. The only
differences are which method to delegate to in the block-given
case (mrb_obj_instance_eval vs. mrb_mod_module_eval) and
which class to use as the target (singleton vs. self-as-class).

Extract the shared logic into object_eval(self, class_eval).
The two top-level dispatchers become one-line wrappers.

Closes #6579, picked from PR by dearblue.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-08 11:51:25 +09:00
Yukihiro "Matz" Matsumoto 817ec1bd90 Merge pull request #6580 from dearblue/File.basename
Improve `File.basename`
2026-05-08 11:41:36 +09:00
Yukihiro "Matz" Matsumoto e4662551a3 Merge pull request #6577 from dearblue/ary_dup
Sharing arrays with `ary_dup()`
2026-05-08 11:15:00 +09:00
Yukihiro "Matz" Matsumoto 90a0fc5797 Merge pull request #6576 from dearblue/ary_replace
Share array entities if possible with `ary.replace(frozen_ary)`
2026-05-08 11:09:50 +09:00
Yukihiro "Matz" Matsumoto 3f709f61dd Merge pull request #6820 from petekinnecom/inheritedOrdering
Fix Class inherited hook ordering
2026-05-08 11:00:48 +09:00
Yukihiro "Matz" Matsumoto bf0915dd03 Merge pull request #6817 from dearblue/array-ext
Integrate `Array#{permutation,combination}` into `Array#__combination`
2026-05-08 10:56:42 +09:00
dearblue df74d57857 array.h: introduce ARY_GETMEM/RARRAY_GETMEM and type ARY_EMBED_PTR
Adds two API helpers that mirror CRuby's RSTRING_GETMEM idiom:

- ARY_GETMEM(a, ptr, len): inside-mruby helper that takes a
  struct RArray* and assigns ptr/len from the embed or heap form.
- RARRAY_GETMEM(a, ptr, len): public wrapper on an mrb_value.

Both expand to a single ARY_EMBED_P check, with a uniqued local so
the array argument is evaluated only once (callers can safely pass
expressions with side effects).

Also type the ARY_NO_EMBED stub of ARY_EMBED_PTR as
((mrb_value*)NULL) instead of integer 0, so it composes cleanly in
pointer expressions like the new ARY_GETMEM.

The build-error issue this originated from (compilation with
MRB_ARY_NO_EMBED) was already fixed differently in master via
#ifndef guards (commit 78658d67e). These additions stand on their
own as new API for downstream gems.

Closes #6712, picked from PR by dearblue.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-08 10:47:58 +09:00
Yukihiro "Matz" Matsumoto 91b60802db mruby-regexp: cap character class count and free named_captures
Two related bugs uncovered by OSS-Fuzz testcase 6692915710853120:

1. add_class allowed unbounded growth of c->classes. Class IDs are
   stored in re_inst.a (uint8_t), so any ID >= 256 silently aliases
   another class via the cast at emit sites. Worse, c->class_capa
   (uint16_t) overflows on doubling past 32768 -> 0, then
   mrb_realloc(..., 0) returns NULL, and the next memset(&c->classes[id])
   segfaults at NULL+offset. Cap with RE_MAX_CLASSES = 256 (the encoding
   limit) and raise via compile_error past that.

2. Once the crash is fixed, the testcase exposes a leak of
   c->named_captures: compile_error frees c->code, c->classes, and
   c->stripped (commit 3f321f09bc) but missed named_captures. Add it
   to the same cleanup block.

Reported by OSS-Fuzz (clusterfuzz testcase 6692915710853120).

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-08 10:32:03 +09:00
Pete Kinnecom ebfdf8ae43 Fix Class inherited hook ordering
In CRuby the inherited method is invoked before Class.new yields to the
block.

An "already initialized error" exception is also removed since the
inherited method is now invoked before initialization and can set
instance variables on a class.
2026-05-07 15:51:30 +00:00
Yukihiro "Matz" Matsumoto 28624ecfd8 mruby-regexp: cap {n}/{n,m} quantifiers to prevent overflow
parse_quantifier read digits via min = min * 10 + d with no upper
bound, allowing patterns like /a{1111558833}/ to overflow int and
trigger signed-integer-overflow UB. Even without UB, the value
flows into compile_quantified's emit loop where it would attempt
to emit a billion copies of the atom.

Add RE_MAX_REPEAT = 32768 (the largest value that still fits in
re_inst.offset, the uint16_t jump field) and reject quantifiers
beyond that during parsing via compile_error. Apply the same cap
to the max field.

Reported by OSS-Fuzz (clusterfuzz testcase 6152367367323648).

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-07 16:25:12 +09:00
Yukihiro "Matz" Matsumoto f267925d45 mruby-socket: translate Winsock errors to errno on Windows
Winsock APIs (socket, bind, connect, accept, recv, send, ...) report
errors via WSAGetLastError() and do not set errno, so mrb_sys_fail
on Windows was reading a stale or zero errno. Result: every socket
failure raised SystemCallError with errno 0 ("Success") instead of
the appropriate Errno::* class.

Add mrb_hal_socket_set_errno_from_last_error() to the HAL:
- POSIX: no-op (failed calls already set errno)
- Windows: maps WSAGetLastError() to a POSIX errno via wsa_to_errno()
  with 32 cases covering the common Winsock error codes; unmapped
  codes fall back to EIO. Each case is #ifdef-guarded against older
  MSVC CRTs that lack a particular Exxx.

In src/socket.c, route the 22 socket-API failure sites through a new
sock_sys_fail() helper that calls the HAL translator before
mrb_sys_fail. Also fix mrb_hal_socket_set_nonblock() on Windows,
which was returning -1 without setting errno after ioctlsocket
failure.

POSIX behavior unchanged (verified: TCPSocket connect refused ->
Errno::ECONNREFUSED, bind to privileged port -> Errno::EACCES, bad
sockopt -> Errno::EOPNOTSUPP).

Closes #6819, reported by Asmod4n.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-07 11:01:46 +09:00
Yukihiro "Matz" Matsumoto ada9ca7236 Merge pull request #6818 from mruby/dependabot/github_actions/github-actions-dependencies-7e7af3d814
build(deps): bump j178/prek-action from 2.0.2 to 2.0.3 in the github-actions-dependencies group
2026-05-07 08:54:57 +09:00
dependabot[bot] 428c9b5b03 build(deps): bump j178/prek-action
Bumps the github-actions-dependencies group with 1 update: [j178/prek-action](https://github.com/j178/prek-action).


Updates `j178/prek-action` from 2.0.2 to 2.0.3
- [Release notes](https://github.com/j178/prek-action/releases)
- [Commits](https://github.com/j178/prek-action/compare/cbc2f23eb5539cf20d82d1aabd0d0ecbcc56f4e3...6ad80277337ad479fe43bd70701c3f7f8aa74db3)

---
updated-dependencies:
- dependency-name: j178/prek-action
  dependency-version: 2.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-06 15:03:04 +00:00
dearblue 3db11cef09 Integrate Array#{permutation,combination} into Array#__combination
Compared to a pure Ruby implementation, this results in faster performance, eliminates recursive calls, and removes the creation of intermediate objects.

The "permutation" implementation in `ary_combination_next()` is slow for C.
However, it does not require a heap other than the index array.
2026-05-06 18:27:16 +09:00
dearblue e7a375d4be Preparations for integrating the implementation of Array#{permutation,combination}
- Modify the `mrb_combination_state` structure to accommodate feature extensions
  - Rename `Array#__repeated_combination` to `__combination`
  - Consolidate integer checks for arguments into `__combination`
  - Since checking for integer types using both `__to_int` and `0 <=>` is redundant, use only `__to_int`
  - Since `__combination` now accepts symbols instead of booleans, the call to `to_enum` has also been consolidated
2026-05-06 18:27:16 +09:00
Yukihiro "Matz" Matsumoto c6836f494a fp_uscale.c: fix uninitialized *fp when exponent is malformed
mrb_read_float jumped past the *fp assignment via `goto done` when
the exponent had no digits (e.g., "5e", "5e+"). It returned TRUE
without setting *fp, leaving the caller (mrb_str_to_dbl etc.) to
return whatever was on the stack. MSan flagged this; on most runs
the uninitialized read happens to yield 0.0, so the bug is silently
incorrect rather than crashing.

Refactor the finalization (compute res from d, final_p, sign, etc.)
to run once after the optional-exponent block. The malformed-exponent
case now falls through using the mantissa-only `final_p = trunc - dp`,
producing the same result strtod gives for the same input ("5e" -> 5.0
with endp at 'e'). Float("5e") still raises because mrb_str_len_to_dbl
rejects trailing characters under badcheck.

Reported by OSS-Fuzz (MSan).

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-06 09:34:41 +09:00
Yukihiro "Matz" Matsumoto 3cc60d31c6 error.h, array.h: support MRB_NAN_BOXING on 32-bit
On 32-bit platforms whose ABI gives 8-byte members 8-byte alignment
(xtensa, ARM, MIPS, PowerPC, ...), MRB_NAN_BOXING failed to build with
"RVALUE size must be within 5 words" because two structs got padded
past the budget:

- struct RBreak: had an existing MRB_USE_RBREAK_VALUE_UNION workaround
  that stores the value as uint32_t[] to avoid forcing 8-byte alignment
  on the struct, but the gate only enabled it for MRB_NO_BOXING.
  Extend to NAN_BOXING + 32-bit, with a NAN_BOXING-specific get/set
  (no separate tt to stash since nan-boxing encodes type in the bits).

- struct RArray: MRB_ARY_NO_EMBED was similarly gated to NO_BOXING;
  embedded mrb_value[] forces 8-byte alignment of the inner union and
  pads the heap-form layout. Extend the gate to NAN_BOXING + 32-bit.

Both gates now name the structural property (32-bit + mrb_value has an
8-byte aligned member) rather than enumerating boxing modes, so adding
new boxing modes won't silently miss this class of bug again.

i386's System V ABI gives uint64_t only 4-byte alignment, hiding the
problem on x86 -m32; -malign-double simulates the strict-alignment ABI
that exhibits the failure, and is what was used to verify the fix.

Closes #6815, reported by dearblue.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-06 07:56:25 +09:00
Yukihiro "Matz" Matsumoto 90ead4bdaf Merge pull request #6816 from dearblue/array-ext
Rename the members of the `mrb_combination_state` structure
2026-05-05 23:24:08 +09:00
dearblue a8c841433f Rename the members of the mrb_combination_state structure
Since these are expressed as "nPk" or "nCk" in mathematics, rename `n` to `k` and `array_size` to `n`.
Additionally, rename the parameters `#__repeated_combination` and `#__combination_init` from `n` to `k`.

However, the parameter `n` in `#repeated_permutation` and `#repeated_combination` remains unchanged to align with CRuby.
2026-05-05 22:25:26 +09:00
Yukihiro "Matz" Matsumoto 3f321f09bc mruby-regexp: fix leak and UAF on compile error paths
compile_error is the chokepoint for all regex-compile errors;
mrb_raisef longjmps out of re_compile, abandoning the stack-local
re_compiler struct. Three connected bugs:

1. Memory leak: c->code and c->classes (grown by emit/add_class
   via mrb_realloc) were never freed before raising, leaking on
   any compile error like /[/. c->stripped was already cleaned up
   here for the same reason; the other two buffers were missed.

2. Use-after-free: c->src aliases c->stripped when RE_FLAG_EXTENDED
   is set, but the original code freed c->stripped before passing
   c->src to mrb_raisef's "%s" formatter. Format the message into
   an mrb_value first (mruby's GC-managed string survives the
   longjmp), then free, then raise.

3. Heap-buffer-overflow: strip_extended returns a non-NUL-terminated
   buffer of size len. Even with format-before-free, "%s" called
   strlen and read past the buffer end. Use mruby's %l directive
   which takes an explicit (char*, size_t) and avoids strlen.

Reported by OSS-Fuzz (clusterfuzz testcase 5394267353972736).

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-05 09:31:38 +09:00
Yukihiro "Matz" Matsumoto ddcbd2dc90 fp_uscale.c: fix shift and clz UB in tiny-float formatting
Two UBSan issues exposed by sprintf("%f", 1e-7) and similar:

1. uscale() shifted hi by c.s without bounding c.s, hitting UB
   when c.s >= 64. The mask line had `c.s & 63`, but the actual
   `hi >> c.s` line did not, so the partial guard was incomplete.
   On x86 the hardware silently masks the shift, producing wrong
   output ("1844674407370.955078" for 1e-7) instead of crashing.
   When c.s >= 64 the value rounds to 0 with sticky=1, so we can
   bail early.

2. count_digits(0) called bits_len64(0) -> clz64(0), which is UB.
   The only other bits_len64 caller already guards d == 0; align
   count_digits with that pattern. Returning 1 (since "0" is one
   digit) preserves output formatting.

Reported by OSS-Fuzz (clusterfuzz testcase 5210395240628224).

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-04 07:53:00 +09:00
Yukihiro "Matz" Matsumoto 479af5c1bd mruby-regexp: bounds-check non-ASCII RE_CHAR in first_set_walk
The first-byte bitmap (bm[16]) is intentionally ASCII-only
(include/re_internal.h:75 documents it as 128 bits / ASCII), and
the matcher at re_exec.c:39 short-circuits for bytes >= 128. But
first_set_walk's RE_CHAR case wrote bm[a >> 3] without checking
a, overflowing the 16-byte stack buffer for any pattern
containing a byte >= 128.

When a >= 128, return FALSE so compute_first_set marks the filter
unusable, matching the bail-out pattern already used for RE_NCLASS
and RE_ANY. The pattern still compiles and matches; only the
first-byte optimization is skipped.

Reported by OSS-Fuzz (clusterfuzz testcase 4909069193510912).

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-03 22:43:22 +09:00
Yukihiro "Matz" Matsumoto c24d01c6fa Merge pull request #6776 from dearblue/array-combination.3
Return nil if a number less than 1 is passed to `Array#__combination_init`
2026-05-02 23:41:04 +09:00
Yukihiro "Matz" Matsumoto a18467da3f Merge pull request #6814 from dearblue/mruby-bin-mrb
Don't use `#puts` in bintest for "mruby-bin-mrb"
2026-05-02 23:37:26 +09:00
dearblue 525ab7a800 Return nil if a number less than 1 is passed to Array#__combination_init
This simplifies the subsequent processing.
2026-05-02 22:16:56 +09:00
dearblue a98c6b62ea Don't use #puts in bintest for "mruby-bin-mrb"
Since `Kernel#puts` is undefined in unit tests, using `Kernel#print` is required.
Previously, for example, running `rake test` with the following build configuration caused the tests to fail.

```ruby
MRuby::Build.new do
  toolchain
  enable_debug
  enable_test
  enable_bintest
  gem core: "mruby-bin-mrb"
end
```
2026-05-02 20:52:00 +09:00
Yukihiro "Matz" Matsumoto 7dfd560df8 mruby-io: cap puts recursion depth to prevent C stack overflow
io_puts_ary recursed unconditionally on nested arrays. For cyclic
arrays (a = []; a << a; puts a) or pathologically deep arrays,
this caused a C stack overflow.

Add a depth cap (IO_PUTS_MAX_DEPTH = 16); on overflow, write
"[...]\n" and return, matching CRuby's behavior on cycles. The
pattern mirrors mruby-set's MAX_NESTED_DEPTH for the same problem
shape (pure C recursion not dispatched as a Ruby method).

Reported by OSS-Fuzz (clusterfuzz testcase 6233530857488384).

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-02 11:27:01 +09:00
Yukihiro "Matz" Matsumoto 8a73faf61e vm.c: refresh ci after mrb_const_set in OP_SETCONST
mrb_const_set may invoke const_added via mrb_funcall_argv, which
re-enters the VM and can reallocate cibase. This invalidates the
local ci pointer (and thus the regs macro that expands to ci->stack),
causing a use-after-free on the next opcode dispatch.

OP_SETMCNST and the OP_GET* / OP_*IDX opcodes already follow this
pattern; align OP_SETCONST with them.

Reported by OSS-Fuzz (clusterfuzz testcase 5886006653157376).

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-02 10:42:41 +09:00
Yukihiro "Matz" Matsumoto adb27caaa2 Merge pull request #6813 from mruby/dependabot/pre_commit/pre-commit-hooks-4c01ccd002
build(deps): bump https://github.com/oxipng/oxipng from v10.1.0 to 10.1.1 in the pre-commit-hooks group
2026-05-01 11:01:42 +09:00
dependabot[bot] 7579a70974 build(deps): bump https://github.com/oxipng/oxipng
Bumps the pre-commit-hooks group with 1 update: [https://github.com/oxipng/oxipng](https://github.com/oxipng/oxipng).


Updates `https://github.com/oxipng/oxipng` from v10.1.0 to 10.1.1
- [Release notes](https://github.com/oxipng/oxipng/releases)
- [Changelog](https://github.com/oxipng/oxipng/blob/master/CHANGELOG.md)
- [Commits](https://github.com/oxipng/oxipng/compare/v10.1.0...v10.1.1)

---
updated-dependencies:
- dependency-name: https://github.com/oxipng/oxipng
  dependency-version: 10.1.1
  dependency-type: direct:production
  dependency-group: pre-commit-hooks
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-30 14:59:05 +00:00
Yukihiro "Matz" Matsumoto 7e73392fc1 Merge pull request #6812 from DavidKorczynski/oss-fuzz-ext
Add new fuzzing harness to be consumed by OSS-Fuzz
2026-04-30 08:55:34 +09:00
David Korczynski 923c2e6a73 Add new fuzzing harness to be consumed by OSS-Fuzz
Adds 6 new fuzzing harnesses to be consumed by OSS-Fuzz. Have confirmed
locally this results in significant coverage gains relative to the
current code coverage in OSS-Fuzz:
https://storage.googleapis.com/oss-fuzz-coverage/mruby/reports/20260427/linux/src/report.html

Signed-off-by: David Korczynski <david@adalogics.com>
2026-04-29 06:49:03 -07:00
Yukihiro "Matz" Matsumoto 7c30c2f62b Merge pull request #6811 from mruby/dependabot/bundler/bundler-dependencies-59f8ea4567 2026-04-29 06:49:35 +09:00
dependabot[bot] fb41438cec build(deps): bump yard
Bumps the bundler-dependencies group with 1 update in the / directory: [yard](https://yardoc.org).


Updates `yard` from 0.9.42 to 0.9.43

---
updated-dependencies:
- dependency-name: yard
  dependency-version: 0.9.43
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: bundler-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-28 14:53:43 +00:00
Yukihiro "Matz" Matsumoto e4cd7e6daf mruby-compiler: place newline before else
The mruby C style places `else` on its own line. Reformat the
remaining `} else if (...)` occurrence in the C action block of
NODE_SYMBOLS dump.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-28 12:56:25 +09:00
Yukihiro "Matz" Matsumoto 16f1f4418a mruby-bigint: place newline before else
The mruby C style places `else` on its own line. Reformat the
remaining `} else {` occurrences.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-28 12:56:25 +09:00
Yukihiro "Matz" Matsumoto d6556195ef boxing_nan.h, boxing_word.h: place newline before else
The mruby C style places `else` on its own line. Reformat the
remaining `} else {` / `} else if (...)` occurrences.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-28 12:56:24 +09:00
Yukihiro "Matz" Matsumoto 1702b89c25 Merge pull request #6787 from dearblue/sprintf 2026-04-28 12:56:07 +09:00
Yukihiro "Matz" Matsumoto 06d236d535 mruby-bin-config: register installer in products instead of bins
`build.bins` is for compiled binaries with bare names; entries are
fed through `exefile()` in `tasks/bin.rake`, which appends
`exts.executable` (e.g. `.exe`) when no extension is present.

When `ENV['OS']` is not `Windows_NT` but the toolchain has
`exts.executable=".exe"` (e.g. cross builds, or environments where
`OS` is unset under visualcpp), `mruby-bin-config` ended up with a
file task at `.../mruby-config` while rake asked for
`.../mruby-config.exe`, aborting with "Don't know how to build task".

Skip the `bins` path and call `build.define_installer` directly with
`mruby_config_path`, mirroring the existing `iscross` branch. The
file task path now always matches the script content's extension
(`.bat` on Windows, no extension elsewhere).

close #6807, reported by UENO, M. (@eunos-1128).

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-25 19:57:44 +09:00
Yukihiro "Matz" Matsumoto baf4145fa1 Merge pull request #6806 from mruby/dependabot/bundler/bundler-dependencies-55d95e1de2 2026-04-24 14:07:17 +09:00
Yukihiro "Matz" Matsumoto b70d160273 Revert "gha: add hosts file workaround for Windows localhost resolution"
This reverts commit eed32b752b.

Windows's Winsock getaddrinfo does not consult the hosts file for
"localhost" because an internal hard-coded rule short-circuits first,
so the Add-Content step had no effect on the failing tests. Remove
the ineffective workaround now that a skip guard replaces it.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-24 08:19:24 +09:00
Yukihiro "Matz" Matsumoto 00e2c2fb29 Revert "gha: start DNS Client service and probe Windows localhost resolution"
This reverts commit 7ae25febbb.

CI runs with this change showed Dnscache was already Running on all
Windows jobs and Resolve-DnsName resolved "localhost" successfully,
yet Winsock getaddrinfo still failed. The service start and probe
had no effect on the failing tests, so they add noise without value.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-24 08:19:15 +09:00
Yukihiro "Matz" Matsumoto 550be1445f Revert "mruby-socket: add temporary getaddrinfo diagnostic for Windows CI"
This reverts commit a69d12aa54.

The diagnostic served its purpose: it isolated the failure to
Winsock's getaddrinfo for the "localhost" hostname. The permanent
fix (skip guard) is now in place, so remove the temporary probe.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-24 08:19:04 +09:00
Yukihiro "Matz" Matsumoto 57ca531f76 mruby-socket: skip localhost-dependent addrinfo tests on Windows
Addrinfo.getaddrinfo("localhost", ...) and Addrinfo.foreach("localhost",
...) crash on GitHub Actions Windows runners (Windows Server 2022 and
2025, both mingw-gcc and MSVC). Diagnostic showed 127.0.0.1 as a
numeric literal resolves fine via Winsock getaddrinfo, but "localhost"
as a hostname returns WSAHOST_NOT_FOUND under every address family
(AF_INET, AF_INET6, AF_UNSPEC, nil). PowerShell Resolve-DnsName on
the same runner succeeds, so the failure is Winsock-specific rather
than OS-level. Earlier workarounds (Dnscache service start, hosts
file append) had no effect.

Skip both tests on Windows with the existing SocketTest.win? guard,
matching the pattern already used for Addrinfo.unix and
Addrinfo#afamily. Linux and macOS coverage is unaffected.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-24 08:18:41 +09:00
Yukihiro "Matz" Matsumoto 7ae25febbb gha: start DNS Client service and probe Windows localhost resolution
Addrinfo.getaddrinfo on the Windows CI runners fails for "localhost"
regardless of address family (diagnostic commit a69d12aa54 showed
127.0.0.1 literal resolves fine while "localhost" never does under
AF_INET, AF_INET6, AF_UNSPEC, or nil). That rules out the earlier
KB4057932/AF_INET hypothesis and points at the Dnscache service
being stopped on the runner image.

Start Dnscache if not already running (and set it to Automatic),
then Resolve-DnsName localhost to print the OS-level resolver view
to the log. Existing hosts file and diagnostic assert stay in place
so the single CI round exposes both the service state and its
effect on getaddrinfo.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-24 01:39:08 +09:00
Yukihiro "Matz" Matsumoto a69d12aa54 mruby-socket: add temporary getaddrinfo diagnostic for Windows CI
Addrinfo.getaddrinfo("localhost", 53, AF_INET, SOCK_STREAM) crashes
on GitHub Actions Windows runners with WSAHOST_NOT_FOUND, while the
same call works on Linux and macOS. Earlier hypotheses (winsock link
missing; hosts file not populated) have been ruled out on CI.

Probe five variants in one run to isolate the failing condition:
numeric literal 127.0.0.1, localhost with AF_UNSPEC, localhost with
nil family, localhost with AF_INET6, and the original AF_INET path.
Results go to stdout via puts so they are visible in CI logs. This
commit is temporary and will be reverted once the real fix lands.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-24 01:20:15 +09:00
Yukihiro "Matz" Matsumoto eed32b752b gha: add hosts file workaround for Windows localhost resolution
GitHub Actions Windows Server 2022/2025 runner images ship with the
localhost entries commented out in C:\Windows\System32\drivers\etc\hosts
and rely on the DNS Client service's built-in rule. That path is subject
to Microsoft KB4057932 (getaddrinfo fails with WSAHOST_NOT_FOUND after
an AF_INET6 negative cache), which makes Addrinfo.getaddrinfo("localhost",
53, AF_INET, ...) in mrbgems/mruby-socket/test/addrinfo.rb flaky on CI.

Prepend explicit 127.0.0.1/::1 localhost entries to the hosts file on
all three Windows CI jobs (two mingw-gcc matrix entries and Windows-VC)
before running the build, so the resolver avoids the DNS negative cache
path and returns the expected IPv4 address.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-24 00:34:37 +09:00
dependabot[bot] e3dadc083c build(deps): bump rake in the bundler-dependencies group
Bumps the bundler-dependencies group with 1 update: [rake](https://github.com/ruby/rake).


Updates `rake` from 13.4.1 to 13.4.2
- [Release notes](https://github.com/ruby/rake/releases)
- [Changelog](https://github.com/ruby/rake/blob/master/History.rdoc)
- [Commits](https://github.com/ruby/rake/compare/v13.4.1...v13.4.2)

---
updated-dependencies:
- dependency-name: rake
  dependency-version: 13.4.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: bundler-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-23 14:58:23 +00:00
Yukihiro "Matz" Matsumoto 2ff6563484 mruby-task: restore Windows link library dropped during HAL migration
Commit be6413f0d86a ("mruby-task: migrate HAL to ports/ directories")
moved the Windows HAL source into mruby-task/ports/win/ but dropped
the linker.libraries declaration that previously lived in
hal-win-task/mrbgem.rake. Both mingw and MSVC builds now fail to link
task_hal.o/obj with undefined references to timeBeginPeriod,
timeEndPeriod, timeSetEvent, and timeKillEvent. Re-declare winmm
under a for_windows? guard.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 23:16:21 +09:00
Yukihiro "Matz" Matsumoto 3f27eac308 mruby-io: restore Windows link library dropped during HAL migration
Commit 9965f11cfe1c ("mruby-io: migrate HAL to ports/ directories")
moved the Windows HAL source into mruby-io/ports/win/ but dropped the
linker.libraries declaration that previously lived in
hal-win-io/mrbgem.rake. Both mingw and MSVC builds now fail to link
with undefined references to closesocket, WSAStartup, WSACleanup,
select, and WSAGetLastError referenced from io.c and io_hal.c.
Re-declare ws2_32 under a for_windows? guard.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 23:16:11 +09:00
Yukihiro "Matz" Matsumoto 22df8d61be mruby-socket: restore Windows link libraries dropped during HAL migration
Commit d9e107c801 ("mruby-socket: migrate HAL to ports/ directories")
moved the Windows HAL source into mruby-socket/ports/win/ but dropped
the linker.libraries declarations that previously lived in
hal-win-socket/mrbgem.rake. Both mingw and MSVC builds now fail to
link with undefined references to WSAStartup, socket, send, and other
Winsock APIs. Re-declare wsock32 and ws2_32 under a for_windows? guard.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 23:16:02 +09:00
Yukihiro "Matz" Matsumoto ad8fc7d918 mruby-bigint: add multi-precision gcd tests
Cover zero operands, power-of-2 fast path, negative operands,
balanced multi-limb pairs with a shared Fibonacci factor, highly
unbalanced pairs (to exercise the Euclidean fallback), and
Fibonacci neighbors (always coprime). Declare a test dependency
on mruby-numeric-ext since Integer#gcd is defined there.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 22:56:44 +09:00
Yukihiro "Matz" Matsumoto 8e91554c6d mruby-bigint: rewrite mpz_gcd main loop as binary Stein algorithm
Replace the classical Euclidean main loop (mpz_mod per iteration)
with Stein's binary GCD: subtract + factor out trailing 2s on
odd-maintained operands. Keep an mpz_mod fallback for heavily
unbalanced pairs (the smaller operand has at least two fewer limbs)
where one long division replaces many Stein subtracts.

The old loop allocated a temporary mpz_t every iteration to hold the
mod result; the Stein loop is allocation-free thanks to the in-place
paths in mpz_sub and mpz_div_2exp. This matches the "memory first"
priority and also happens to be faster on typical inputs because
mpz_mod's setup cost dominates when the quotient is small (the
classical Fibonacci-neighbor worst case).

Also add a small mpz_swap helper used by the new loop.

Benchmark (bin/mruby benchmark/bm_bigint_gcd.rb, median of 3):

  case                       before    after    ratio
  single-limb                 42 ms    50 ms    1.19  (fast-path
                                                      unchanged;
                                                      noise)
  fib(200) vs fib(201)        82 ms    23 ms    0.28
  balanced ~700-bit shared    46 ms    32 ms    0.70
  unbalanced big vs small     35 ms    28 ms    0.80
  power-of-2 path             38 ms    42 ms    1.11  (fast-path
                                                      unchanged;
                                                      noise)
  balanced ~2800-bit shared   19 ms    15 ms    0.79

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 22:52:05 +09:00
Yukihiro "Matz" Matsumoto ec55e0d0f1 mruby-bigint: correct mpz_gcd comment to describe hybrid behavior
The preceding comment claimed "Binary GCD (Stein's algorithm)",
but the multi-limb main loop is classical Euclidean using mpz_mod.
Only the prelude (factoring out common 2s, single-limb and
power-of-2 fast paths) is Stein-flavored. Describe what the code
actually does so readers are not misled.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 22:36:18 +09:00
Yukihiro "Matz" Matsumoto 7e8d74793c doc/guides/rom-method-table.md: sync with new MRB_MT_ENTRY shape
Per gemini-code-assist review on #6790: the documentation still
showed the pre-#6790 union mrb_mt_ptr member order (proc first)
and the C99 designated-initializer form of MRB_MT_ENTRY.  Update
both to match the merged code, and add a note explaining why func
must come first in the union.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:39 +09:00
Yukihiro "Matz" Matsumoto 81bcd4e931 mruby-bigint: simplify MPZ_CTX_INIT with positional initializer
Per gemini-code-assist review on #6791: replace member-by-member
assignment with C89/C++98-style positional aggregate initialization,
keeping the macro a single declaration and matching the existing
pool_storage initializer style in the same macro.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:39 +09:00
Yukihiro "Matz" Matsumoto bbf7c43c37 array.c: restore arena around heap sort per-call protections
heapify() and heap_delete_root() call mrb_gc_protect() to guard a
C-local mrb_value across potential GC points inside sort_cmp().
They were missing the matching mrb_gc_arena_save/restore pair, so
each call leaked one arena slot.  For O(n log n) heapify calls under
MRB_GC_FIXED_ARENA this overflows the 100-slot arena and raises
NoMemoryError, e.g. during Array#repeated_permutation tests.

Wrap each function body with mrb_gc_arena_save/restore, matching
the pattern already used in insertion_sort() above.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:39 +09:00
Yukihiro "Matz" Matsumoto 449040400a mruby-regexp: keep MatchData source/regexp GC-reachable
The mrb_match_data struct stores `source` and `regexp` as plain
mrb_value members of a C-allocated struct, which the GC does not
scan.  Under MRB_GC_STRESS the source string could be collected
while the MatchData was still alive, causing md[0] to read freed
memory (observed as "\xff\xff\xff").

Also stash source and regexp as instance variables on the MatchData
object so they remain reachable via the object's iv_tbl during GC.
The C struct members continue to provide fast direct access, and
no other call sites need to change.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:39 +09:00
Yukihiro "Matz" Matsumoto eb5480a6b3 hash.c: fix float_hash_code() for zero values
Remove early return of hardcoded 0 for zero floats. Instead, normalize
-0.0 to +0.0 and pass through mrb_byte_hash() for better distribution.
The previous second condition (f == -0.0) was dead code since IEEE 754
-0.0 == 0.0 is true.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:39 +09:00
Yukihiro "Matz" Matsumoto b3777110ab hash.c, string.c: improve hash function quality
Use FNV-1a (xor-then-multiply) instead of FNV-1 for better avalanche
in byte hashing. Strengthen the hash finalizer in mrb_obj_hash_code()
with multiply-xorshift to improve distribution for integer and symbol
keys with power-of-two table sizes.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:38 +09:00
Yukihiro "Matz" Matsumoto 86e274f759 irep.h: remove "not yet supported" comment from IREP_TT_BIGINT
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:38 +09:00
Yukihiro "Matz" Matsumoto d212c7be6c bm_ao_render.rb: use pack() for binary PPM output
Replace printf("%c", val) with pack("CCC") for outputting raw
bytes. printf("%c") with values >= 128 produces invalid UTF-8
on CRuby, corrupting the PPM output. Also remove unused
arguments from the "255\n" format string.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:38 +09:00
Yukihiro "Matz" Matsumoto 909f5a070f hw-adc: add ADC peripheral gem for embedded platforms
Add hw-adc gem with analog-to-digital conversion support:
- Common Ruby API with read, read_raw, and read_voltage
- ports/esp32/ using ESP-IDF ADC oneshot driver
- ports/rp2040/ using Pico SDK ADC hardware

Based on picoruby-adc by HASUMI Hitoshi.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:38 +09:00
Yukihiro "Matz" Matsumoto ce4bc77aaf hw-pwm: add PWM peripheral gem for embedded platforms
Add hw-pwm gem with pulse width modulation support:
- Common Ruby API with frequency, duty, period_us, pulse_width_us
- ports/esp32/ using LEDC peripheral
- ports/rp2040/ using Pico SDK PWM hardware

Based on picoruby-pwm by HASUMI Hitoshi.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:38 +09:00
Yukihiro "Matz" Matsumoto 5931a96a17 hw-spi: add SPI peripheral gem for embedded platforms
Add hw-spi gem with SPI communication support:
- Common Ruby API with write, read, and full-duplex transfer
- ports/esp32/ using ESP-IDF SPI master driver
- ports/rp2040/ using Pico SDK

Based on picoruby-spi by HASUMI Hitoshi.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:38 +09:00
Yukihiro "Matz" Matsumoto be6413f0d8 mruby-task: migrate HAL to ports/ directories
Move hal-posix-task and hal-win-task into
mruby-task/ports/posix/ and mruby-task/ports/win/.
Remove HAL auto-detection logic from mrbgem.rake. Update
cosmopolitan.rb to use conf.ports :posix instead of explicit
hal-* gem references.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:37 +09:00
Yukihiro "Matz" Matsumoto d9e107c801 mruby-socket: migrate HAL to ports/ directories
Move hal-posix-socket and hal-win-socket into
mruby-socket/ports/posix/ and mruby-socket/ports/win/.
Remove HAL auto-detection logic from mrbgem.rake.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:37 +09:00
Yukihiro "Matz" Matsumoto 886bf270f0 mruby-dir: migrate HAL to ports/ directories
Move hal-posix-dir and hal-win-dir into mruby-dir/ports/posix/
and mruby-dir/ports/win/. Remove HAL auto-detection logic from
mrbgem.rake.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:37 +09:00
Yukihiro "Matz" Matsumoto 9965f11cfe mruby-io: migrate HAL to ports/ directories
Move hal-posix-io and hal-win-io into mruby-io/ports/posix/ and
mruby-io/ports/win/. Platform sources are now compiled
automatically based on conf.ports setting. Remove HAL
auto-detection logic from mrbgem.rake.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:37 +09:00
Yukihiro "Matz" Matsumoto a8c82b506f hw-uart: consolidate platform gems into ports/ directories
Move hw-esp32-uart and hw-rp2040-uart into hw-uart/ports/esp32/
and hw-uart/ports/rp2040/ using the new ports build system.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:37 +09:00
Yukihiro "Matz" Matsumoto 53a43e9d36 hw-gpio: consolidate platform gems into ports/ directories
Move hw-esp32-gpio and hw-rp2040-gpio into hw-gpio/ports/esp32/
and hw-gpio/ports/rp2040/ using the new ports build system.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:37 +09:00
Yukihiro "Matz" Matsumoto b8eccc7a38 hw-i2c: consolidate platform gems into ports/ directories
Move hw-esp32-i2c and hw-rp2040-i2c into hw-i2c/ports/esp32/
and hw-i2c/ports/rp2040/ using the new ports build system.
Platform sources are now compiled automatically based on
conf.ports setting. Separate platform gems are no longer needed.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:36 +09:00
Yukihiro "Matz" Matsumoto 0e44db114c build: add ports support for platform-specific gem sources
Add conf.ports method to specify target platform tags (e.g.,
:esp32, :rp2040, :posix). Gems with matching ports/<name>/
directories will automatically compile those sources.

Host builds auto-detect :posix or :win when not explicitly set.
Cross builds require explicit specification. Existing gems
without ports/ directories are unaffected.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:36 +09:00
Yukihiro "Matz" Matsumoto 2b5a389de3 hw-uart: add UART peripheral gems for embedded platforms
Add three new gems for UART serial communication:
- hw-uart: common Ruby API, C bindings, ring buffer, and HAL header
- hw-esp32-uart: ESP32 HAL using ESP-IDF UART driver with FreeRTOS
  RX task
- hw-rp2040-uart: RP2040 HAL using Pico SDK with IRQ-driven RX

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:36 +09:00
Yukihiro "Matz" Matsumoto bdc3a65601 mruby-io: call mrb_hal_io_init/final from gem_init/gem_final
The HAL init/final functions were defined in hal-posix-io and
hal-win-io but never called. On Windows, hal-win-io performs
WSAStartup/WSACleanup in these functions, which was not being
invoked by mruby-io unlike other HAL gems (dir, socket, task).

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:36 +09:00
Yukihiro "Matz" Matsumoto 0ff41e4e28 hw-gpio: add GPIO peripheral gems for embedded platforms
Add three new gems for GPIO pin control:
- hw-gpio: common Ruby API, C bindings, and HAL header
- hw-esp32-gpio: ESP32 HAL using ESP-IDF GPIO driver
- hw-rp2040-gpio: RP2040 HAL using Pico SDK

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:36 +09:00
Yukihiro "Matz" Matsumoto 1ed52461e8 hw-i2c: add I2C peripheral gems for embedded platforms
Add three new gems for I2C communication:
- hw-i2c: common Ruby API, C bindings, and HAL header
- hw-esp32-i2c: ESP32 HAL using ESP-IDF I2C master driver
- hw-rp2040-i2c: RP2040 HAL using Pico SDK

The HAL API provides init, read, write, and atomic write_read
(repeated START) operations. Platform gems depend on hw-i2c
and are only compiled when explicitly included in build config.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:35 +09:00
Yukihiro "Matz" Matsumoto 416793db3d mruby-sprintf: use mrb_uint cast instead of uint64_t
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:35 +09:00
Yukihiro "Matz" Matsumoto 6f202d6c2b mruby-io: use mrb_int consistently in IO HAL interface
Replace all int64_t, uint64_t, uint32_t, and int32_t with mrb_int
in the HAL struct, timeval, and function signatures. All values
originate from or end up as mrb_int at the Ruby layer. Also removes
dead overflow checks in callers and the stdint.h dependency from
io_hal.h.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:35 +09:00
Yukihiro "Matz" Matsumoto ad1fd9285c mirb.c: add fileno() to _fileno() mapping for MSVC
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:35 +09:00
Yukihiro "Matz" Matsumoto 29f84030ca bigint.c: fix signed/unsigned comparison warning in base conversion
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:35 +09:00
Yukihiro "Matz" Matsumoto b70c393039 mruby-io: use mrb_int for mrb_hal_io_readlink() return type
int64_t was unnecessary; readlink(2) returns ssize_t (bounded by
PATH_MAX), and the Windows HAL just raises NotImplementedError.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:35 +09:00
Yukihiro "Matz" Matsumoto 7ae73ac7e4 class.c: extract mrb_get_args fast path validator to inline function
Replace the inline switch with a separate fast_fmt_ok() function that
maps each format character to a validity code (0=invalid, 1=arg spec,
2=separator). Modern compilers generally lower this to a jump table,
so the per-character cost remains effectively O(1).

Keeping this as a switch (instead of a C99 array-index designator
lookup table) also lets the file compile cleanly as C++.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:34 +09:00
Yukihiro "Matz" Matsumoto 966aa9512e class.c: add fast path in mrb_get_args for simple format strings
Skip the two-pass format scanning when the format string contains
only simple specifiers (o, S, i, n, z, b, f, A, H, c, s, a) with
optional '|' separator. Validates all specifiers before consuming
va_list to ensure safe fallback to the slow path.

Covers ~50% of all mrb_get_args call sites in the codebase (175
of 353). Reduces per-call argument parsing overhead by ~30-40%.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:34 +09:00
Yukihiro "Matz" Matsumoto 726d8febf3 mruby-regexp: cache Pike VM state in pattern to avoid per-exec malloc
Pre-allocate visited array and thread lists at compile time and
reuse them across re_exec calls. A cache_in_use flag detects
re-entrancy and falls back to malloc when needed.

Eliminates 3 malloc + 3 free per NFA execution for the common
non-re-entrant case. Combined with the literal fast path, brings
literal match? from 4.7x to 2.5x vs CRuby.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:34 +09:00
Yukihiro "Matz" Matsumoto 034a64346c mruby-regexp: add literal pattern fast path bypassing NFA
Pure literal patterns (/hello/, /abc/) are detected at compile
time and matched using memchr+memcmp directly, completely
bypassing Pike VM setup (no malloc, no visited array, no thread
lists). Engine-only time drops from ~400ns to ~80ns.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:34 +09:00
Yukihiro "Matz" Matsumoto 8624764bca mruby-regexp: implement gsub/sub/scan core in C
Move the match loop for non-block gsub, sub, and scan from Ruby
to C. Key improvements:
- re_exec called directly in a loop without MatchData per match
- MatchData created only once (for $~ of the last match)
- Replacement \-escapes processed in C (apply_replacement)
- No intermediate string objects for pre_match/post_match
- Block variants remain in Ruby to avoid VM callbacks

Performance improvement (vs CRuby ratio):
  gsub simple: 5.0x -> 2.9x
  scan words:  5.8x -> 1.9x

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:34 +09:00
Yukihiro "Matz" Matsumoto 337b5906a2 mruby-regexp: add first-byte bitmap for fast position skipping
Compute a 128-bit bitmap of bytes that could start a match.
For patterns like /cat|dog|fox/ the bitmap contains only {b,c,d,f},
skipping positions where no alternative can match. For /\d+/ only
{'0'-'9'} are set.

Used when no literal prefix is available (alternation, character
class patterns). Falls back gracefully when too many bytes match.

Key improvements vs CRuby ratio:
  alternation miss: 9.5x -> 3.6x
  \d+ medium string: 3.0x -> 1.4x

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:33 +09:00
Yukihiro "Matz" Matsumoto 466e4ad84a mruby-regexp: defer pool_copy until character actually matches
Move pool_copy inside each match condition so captures are only
copied for threads that advance to the next step, skipping the
copy for non-matching threads.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:33 +09:00
Yukihiro "Matz" Matsumoto 1641c8c06f mruby-regexp: add literal prefix skip for fast string search
Extract the leading literal bytes from compiled bytecode and use
memchr + memcmp to skip positions where the prefix cannot match.
Both Pike VM and backtracking engine benefit.

For /needle/ in a 2006-char string: 29x faster (1.97s -> 0.07s),
now on par with CRuby/Oniguruma.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:33 +09:00
Yukihiro "Matz" Matsumoto 355c68f487 mruby-regexp: rename has_nongreedy to needs_backtrack
The flag is set not only for non-greedy quantifiers but also for
lookahead, lookbehind, and backreferences. The new name accurately
reflects its purpose: indicating that the backtracking engine is
required instead of the Pike VM.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:33 +09:00
Yukihiro "Matz" Matsumoto 13e63657a4 mruby-regexp: use array join in __sub_replace to avoid O(n^2)
Same pattern as the earlier gsub fix: collect parts in an array
and join at the end instead of repeated string concatenation.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:33 +09:00
Yukihiro "Matz" Matsumoto 6edef4e7e6 mruby-regexp: use dynamic captures allocation in exec_match()
Replace fixed RE_MAX_CAPTURES*2 (256 bytes) stack array with
malloc sized to actual pat->num_captures*2. Consistent with
the Pike VM's dynamic ncap-sized pool.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:33 +09:00
Yukihiro "Matz" Matsumoto 1f0809aad3 mruby-regexp: consolidate MatchData#captures and #to_a
Both methods had identical loop bodies, differing only in the
starting group index (1 vs 0). Extracted matchdata_to_ary()
with a from parameter.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:32 +09:00
Yukihiro "Matz" Matsumoto 2ef3a7c21e mruby-regexp: extract exec_match() to consolidate match methods
Regexp#match and Regexp#=~ shared most of their logic (get pattern,
execute, create MatchData, set globals). Extracted into exec_match()
internal function. Regexp#=~ now calls exec_match() and reads the
match position from the returned MatchData.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:32 +09:00
Yukihiro "Matz" Matsumoto 16b73ec67b mruby-regexp: extract get_iflags() helper to reduce duplication
The pattern of reading @flags IV and converting to uint32_t was
repeated in 6 methods. Consolidated into a single helper function.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:32 +09:00
Yukihiro "Matz" Matsumoto 3d8ccee7a8 vm.c: optimize cipush/cipop for common cases
CI_PROC_SET: split NULL/non-NULL proc paths so the compiler can
eliminate the CFUNC/ALIAS checks when proc is a compile-time NULL
(8 of 11 cipush call sites).

cipop: add fast path for the common case where no env and no blk
are set. skips ci_env_set, orphan check, and env_unshare entirely.
most simple method calls (no blocks, no closures) take this path.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:32 +09:00
Yukihiro "Matz" Matsumoto 101f8c69a1 mruby-regexp: implement fixed-length lookbehind assertions
Add (?<=...) positive and (?<!...) negative lookbehind support.
The sub-pattern must have a fixed byte length (no quantifiers or
alternation), computed at compile time and stored in the instruction.
At execution time, the engine backs up by that many bytes and runs
the sub-pattern forward. Maximum lookbehind length is 255 bytes.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:32 +09:00
Yukihiro "Matz" Matsumoto 4ded345ebb mruby-regexp: use array join in gsub to avoid O(n^2) concatenation
Collect replacement parts in an array and join at the end instead
of repeated string += which creates intermediate string objects.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:31 +09:00
Yukihiro "Matz" Matsumoto 9d955ba75f mruby-regexp: skip capture tracking in match-only path
When match? calls re_exec with captures=NULL, the Pike VM now
skips all capture pool operations: no pool_copy, no RE_SAVE
writes, no pool compaction. Only a single dummy pool slot is
allocated. This significantly reduces work for boolean matching.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:31 +09:00
Yukihiro "Matz" Matsumoto a2edda173b mruby-regexp: cache $1-$9 symbol IDs for match globals
Pre-intern the $1-$9 symbols on first use instead of calling
mrb_intern_cstr (which computes strlen + hash) on every match.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:31 +09:00
Yukihiro "Matz" Matsumoto 4579caa7d8 mruby-regexp: optimize Pike VM with pooled captures and generation counter
Major changes to the NFA execution engine:

- Thread captures stored in a flat pool sized to actual ncap
  (e.g. 4 ints for 1 capture group vs 64 fixed), dramatically
  reducing per-thread copy cost
- Generation counter for visited[] eliminates per-step memset
  of the entire bytecode-length array
- Pool compaction between steps reclaims dead thread slots
- Backtracking engine also uses dynamic ncap-sized captures

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:31 +09:00
Yukihiro "Matz" Matsumoto 5a158d32a8 mruby-regexp: support \& \` \' \+ \\ in sub/gsub replacements
Replacement strings now support:
  \& = full match, \` = pre_match, \' = post_match,
  \+ = last successful capture, \\ = literal backslash.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:31 +09:00
Yukihiro "Matz" Matsumoto 26dc5f76ea mruby-regexp: add MatchData#string, #regexp, and #to_s
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:31 +09:00
Yukihiro "Matz" Matsumoto 36a0f83db3 mruby-regexp: accept Regexp argument in Regexp.new
Regexp.new(regexp) copies the source and flags from the given
Regexp object, matching CRuby behavior.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:30 +09:00
Yukihiro "Matz" Matsumoto 893cb4edc4 mruby-regexp: fix Regexp#options to return Ruby constant values
Internal flags (MULTILINE=2, DOTALL=4, EXTENDED=8) differ from
Ruby constants (EXTENDED=2, MULTILINE=4). Convert in C instead
of returning raw internal flags. Also add Regexp#casefold?.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:30 +09:00
Yukihiro "Matz" Matsumoto 3f627c0d7d mruby-regexp: update README for x flag support
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:30 +09:00
Yukihiro "Matz" Matsumoto 78d761addf mruby-regexp: implement extended mode (x flag)
The x flag ignores unescaped whitespace and #comments in patterns,
making complex regexps more readable. Whitespace inside character
classes [...] remains literal. Implemented as a preprocessing step
that strips whitespace/comments before compilation.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:30 +09:00
Yukihiro "Matz" Matsumoto 0ca3192c9f mruby-regexp: implement Regexp#==, Regexp#eql?, and Regexp#hash
Two regexps are equal when they have the same source and flags.
Hash is computed from source string hash mixed with flags.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:30 +09:00
Yukihiro "Matz" Matsumoto 4307461e58 gc.c: add symbol_count and dynamic_symbol_count to GC.stat
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:29 +09:00
Yukihiro "Matz" Matsumoto dab150007f mruby-regexp: implement Regexp#to_s in CRuby-compatible format
Regexp#to_s now returns (?flags:source) format (e.g. "(?i:abc)")
instead of the /source/flags format used by Regexp#inspect.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:29 +09:00
Yukihiro "Matz" Matsumoto 4d81275083 mruby-regexp: add $1-$9 globals and include in stdlib gembox
- $1-$9 global variables set by Regexp#match and Regexp#=~
- $1-$9 cleared to nil on match failure
- add mruby-regexp to stdlib.gembox (auto-included in standard builds)
- remove duplicate gem entry from host-debug.rb

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:29 +09:00
Yukihiro "Matz" Matsumoto 117af56bc2 mruby-regexp: add README.md
document supported syntax, Ruby API, engine architecture,
limitations, configuration, and license.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:29 +09:00
Yukihiro "Matz" Matsumoto 3c68e49178 mruby-regexp: add lookahead assertions (?=...) and (?!...)
implement positive and negative lookahead in the backtracking engine:
- (?=pattern): succeeds if pattern matches at current position
  without consuming characters
- (?!pattern): succeeds if pattern does NOT match at current position

new bytecodes RE_LOOKAHEAD and RE_NEG_LOOKAHEAD implemented in
the backtracking engine via nested bt_match calls.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:29 +09:00
Yukihiro "Matz" Matsumoto 7283560215 mruby-regexp: add named captures (?<name>...)
support named capture groups in patterns:
- compiler parses (?<name>...) syntax and builds name table
- MatchData#[:name] and MatchData#["name"] access by name
- MatchData#named_captures returns {name => value} hash
- Regexp#named_captures returns {name => group_number} hash
- named captures stored in mrb_regexp_pattern for GC safety

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:28 +09:00
Yukihiro "Matz" Matsumoto 8d92379d7c mruby-regexp: fix non-greedy quantifiers (*?, +?, ??)
non-greedy patterns now correctly match the shortest possible
string. patterns with non-greedy quantifiers are dispatched to
the backtracking engine which naturally handles non-greedy
semantics.

the Pike VM continues to be used for purely greedy patterns
(O(n*m) guarantee).

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:28 +09:00
Yukihiro "Matz" Matsumoto 23b2d24cf5 mruby-regexp: add backtracking engine for backreferences
add a recursive backtracking engine that handles \1-\9
backreferences. the Pike VM (NFA) is used for patterns without
backreferences; patterns with backreferences automatically
fall back to the backtracking engine.

the backtracking engine has a step limit (MRB_REGEXP_STEP_LIMIT,
default 1M) to prevent ReDoS on pathological patterns.

also adds SAVE backtracking (save/restore capture positions on
failed branches) for correct submatch tracking.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:28 +09:00
Yukihiro "Matz" Matsumoto 6deafd810f mruby-regexp: add edge case tests and improve coverage
add tests for: empty pattern, nested captures, word boundary \b,
non-capturing groups (?:), sub/gsub with block, scan with captures,
split with regexp, case/when with regexp, date reformatting.

known limitation: non-greedy quantifiers (*?, +?) currently behave
as greedy. needs match priority tracking (TODO for Phase 2).

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:28 +09:00
Yukihiro "Matz" Matsumoto 3bfb27b999 mruby-regexp: add /regex/ literal support, $~, Regexp.compile
- /regex/ literal syntax now works (compiler generates Regexp.compile)
- Regexp#match and Regexp#=~ set $~ global variable
- Regexp.last_match(n) for accessing capture groups
- Regexp.compile as alias for Regexp.new
- Regexp#options

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:27 +09:00
Yukihiro "Matz" Matsumoto 1cfa153ff3 mruby-regexp: add built-in regexp engine with Pike VM
implement a lightweight NFA-based regular expression engine for mruby:

engine (src/re_compile.c, src/re_exec.c, src/re_utf8.c):
- Pike VM (Thompson NFA simulation) with O(n*m) time guarantee
- ReDoS-resistant by design (no backtracking for basic patterns)
- supports: literals, ., *, +, ?, {n,m}, [], [^], |, ()
- character classes: \d, \w, \s and negations
- anchors: ^, $, \A, \z, \Z, \b, \B
- flags: i (ignorecase), m (multiline/dotall)
- captures with MatchData

Ruby API (src/regexp.c, mrblib/string_regexp.rb):
- Regexp.new, #match, #match?, #=~, #===, #source, #inspect
- Regexp.escape, Regexp::IGNORECASE/MULTILINE constants
- MatchData#[], #captures, #to_a, #begin, #end, #pre_match, #post_match
- String#match, #match?, #=~, #sub, #gsub, #scan, #split

~1700 lines of C + ~120 lines of Ruby. no external dependencies.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:27 +09:00
Yukihiro "Matz" Matsumoto 30e41242ec doc/internal/gc.md: add practical tuning examples
add workload-specific GC tuning advice based on benchmark data:
- allocation-heavy: interval_ratio 400 for ~12% improvement
- real-time: step_limit for bounded pause times
- large buffers: malloc_threshold
- diagnosing GC overhead with GC.stat

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:27 +09:00
Yukihiro "Matz" Matsumoto 49dff412b5 gc.c: extract mrb_obj_alloc_core() for internal allocation
split mrb_obj_alloc() into type-validation wrapper and allocation
core (mrb_obj_alloc_core). internal callers (mrb_proc_new,
mrb_env_new) use the core directly, skipping 15+ lines of type
validation per allocation.

most impactful for workloads with heavy Proc/Env allocation
(lambda calculus, block-intensive code).

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:27 +09:00
Yukihiro "Matz" Matsumoto f7d7bbef43 array.c: add string-specialized fast path for Array#sort!
when all elements are plain String (not subclass) and no block is
given, use specialized sort that calls mrb_str_cmp() directly,
bypassing sort_cmp overhead (GC arena, type dispatch, array
modification check).

includes subclass check to ensure String#<=> is not overridden.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:27 +09:00
Yukihiro "Matz" Matsumoto 5364c4167e array.c: add integer-specialized fast path for Array#sort!
when all elements are integers and no block is given, use
specialized heapify/insertion_sort that compare mrb_int values
directly, bypassing sort_cmp entirely. this eliminates per-comparison
overhead of GC arena save/restore, type checking, and array
modification checks.

the pre-scan to detect all-integer arrays is O(n), negligible
compared to O(n log n) sort. non-integer and block sorts are
unaffected.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:26 +09:00
Yukihiro "Matz" Matsumoto 02bb943960 array.c: optimize heap sort with hole-style sift-down and Floyd's method
two improvements to Array#sort!'s heap sort:

1. hole-style sift-down: save root value, move larger children up
   one at a time, write saved value once at the end. reduces
   assignments from 3 per level (swap) to 1 per level (move).

2. Floyd's bottom-up heap deletion: during extraction phase, sift
   the hole down to a leaf using only child-child comparisons
   (~1 comparison per level), then sift up to find the correct
   position. this reduces average comparisons from ~2 log n to
   ~log n per extraction, nearly halving the total comparison
   count for the sort.

both changes preserve O(n log n) worst case and O(1) extra space.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:26 +09:00
Yukihiro "Matz" Matsumoto e292d7a6c4 symbol.c: implement lazy symbol GC (mark-sweep)
when dynamic symbol count reaches MRB_SYMBOL_MAX, run a mark-sweep
pass over all live objects to identify referenced symbols. sweep
unreferenced dynamic symbols, freeing their individually-allocated
string data and marking symtbl slots as tombstones.

mark phase traverses:
- all heap objects (method tables, IV tables, arrays, hashes, envs)
- VM stack values (MRB_TT_SYMBOL)
- call stack method IDs (ci->mid)
- root and current context

after sweep, rebuild hash table to maintain valid collision chains.

this completes the A+ symbol GC plan: the limit acts as a GC
trigger rather than a hard cap. unreferenced DoS symbols are
reclaimed, allowing legitimate code to continue.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:26 +09:00
Yukihiro "Matz" Matsumoto cb64a0b4a4 symbol.c: use individual malloc for dynamic symbol strings
dynamic symbols (created via to_sym, send, etc.) now use
mrb_malloc() instead of sym_pool_alloc(). this makes them
individually freeable by future symbol GC.

static symbols (presym, mrb_intern_static, literals) continue
to use the pool allocator for compact storage.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:26 +09:00
Yukihiro "Matz" Matsumoto afc0753c1d symbol.c: add dynamic symbol limit (MRB_SYMBOL_MAX)
track dynamic (runtime-created) symbols separately from presyms,
inline symbols, and static C API symbols. raise RuntimeError when
the dynamic symbol count exceeds MRB_SYMBOL_MAX (default 4096).

this prevents DoS attacks via unbounded symbol creation (e.g.
"str".to_sym in a loop). presyms and inline symbols are not
counted toward the limit.

infrastructure for future symbol GC: sym_flags array tracks
per-symbol metadata (SYM_FL_DYNAMIC flag).

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:26 +09:00
Yukihiro "Matz" Matsumoto 3f8e13da6f codegen.c: consolidate while/until loop codegen
merge codegen_while/codegen_until into codegen_loop, and
codegen_while_mod/codegen_until_mod into codegen_loop_mod.
each pair differed only in swapped constant-condition checks
(true_always/false_always) and jump opcode (OP_JMPNOT/OP_JMPIF).

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:26 +09:00
Yukihiro "Matz" Matsumoto 65e24f4083 codegen.c: consolidate codegen_dot2/codegen_dot3 into codegen_range
the two functions differed only in OP_RANGE_INC vs OP_RANGE_EXC.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:25 +09:00
Yukihiro "Matz" Matsumoto 0b79c70935 dump.c: consolidate error handling in mrb_dump_irep_cfunc()
replace four repeated `mrb_free(mrb, bin); return MRB_DUMP_WRITE_FAULT`
sequences with a single goto-based cleanup path.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:25 +09:00
Yukihiro "Matz" Matsumoto 16fbd56e4b mruby-task: extract task_create_common() from Task.new and mrb_create_task()
both functions shared identical task allocation, context
initialization, queue insertion, and priority preemption logic.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:25 +09:00
Yukihiro "Matz" Matsumoto 0f47249963 class.c: clear const cache on include/prepend
mrb_include_module() and mrb_prepend_module() did not invalidate
the constant cache. stale cache entries caused incorrect constant
resolution after include changed the ancestor chain.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:25 +09:00
Yukihiro "Matz" Matsumoto 3b85d48f89 class.c: fix module/class reopening via include
mrb_vm_define_module() and mrb_vm_define_class() incorrectly
reopened modules/classes accessible through include rather than
creating new ones. CRuby only reopens modules directly defined
on the outer scope.

the internal define_module()/define_class() use
mrb_const_defined_at() which walks ancestors for Object class.
bypass them and create modules/classes directly in the VM path.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:25 +09:00
Yukihiro "Matz" Matsumoto f3cd991771 mruby-enum-lazy: add Enumerator::Lazy#tap_each
add tap_each method that yields each element for side effects
(e.g. logging, debugging) and passes it through unmodified.
see https://bugs.ruby-lang.org/issues/21520

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:25 +09:00
Yukihiro "Matz" Matsumoto d2caa144be cdump.c: consolidate sym_name_with_*_p into sym_name_with_suffix_p
three functions differed only in the trailing character check ('=',
'?', '!'). replace with a single parameterized function.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:24 +09:00
Yukihiro "Matz" Matsumoto f1a6274c34 vm.c: extract vm_call_proc() to consolidate OP_CALL and OP_BLKCALL
both opcodes share identical proc dispatch logic (alias resolution,
callinfo setup, cfunc/irep branching). the only difference is how
nargs is computed (ci_bidx vs operand b).

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:24 +09:00
Yukihiro "Matz" Matsumoto 6a6e2b48ac vm.c: replace mrb_funcall_argv() with goto L_SEND_SYM in OP_MATHILV
avoid re-entrant VM call from C; use the same dispatch pattern as
OP_MATH and OP_MATHI for consistency.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:24 +09:00
Yukihiro "Matz" Matsumoto 82bb954c23 vm.c: extract vm_define_method() to consolidate OP_TDEF and OP_SDEF
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:24 +09:00
Yukihiro "Matz" Matsumoto 309f450bab gc.c: use actual work done for debt repayment in incremental step
Decrement gc_debt by the actual number of objects processed
instead of the fixed GC_STEP_SIZE. This makes step_ratio
directly affect debt repayment: larger steps repay more debt,
naturally reducing GC invocation frequency.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:24 +09:00
Yukihiro "Matz" Matsumoto 851da984b4 gc.c: use :debt instead of :threshold in GC.stat
Expose gc_debt directly as :debt in GC.stat without sign negation.
The debt model has no threshold ceiling, so :threshold was a
misleading name. Negative debt means credit, positive means GC
is behind on collection work.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:24 +09:00
Yukihiro "Matz" Matsumoto 4d03f40204 doc/internal/gc.md: update for debt model and new tuning parameters
Document the debt-based GC trigger model, malloc threshold,
step limit, GC.stat, and tuning guide.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:23 +09:00
Yukihiro "Matz" Matsumoto 878ecd9b09 gc.c: expose negated gc_debt as :threshold in GC.stat
Use -gc_debt as the :threshold key in GC.stat for familiarity.
Positive means credit remaining, negative means GC is overdue.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:23 +09:00
Yukihiro "Matz" Matsumoto f0b3fdfb14 gc.c: replace threshold model with debt-based GC trigger
Replace the threshold-based GC trigger (gc->threshold vs gc->live)
with a debt model (gc->gc_debt). Each allocation increments debt;
each GC step decrements by GC_STEP_SIZE. When a cycle completes,
credit is proportional to live_after_mark * interval_ratio, giving
a natural feedback loop that adapts to allocation rate.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:23 +09:00
Yukihiro "Matz" Matsumoto 4b866a84da gc.c: add step_limit and malloc_threshold for GC tuning
GC.step_limit caps the per-step work in incremental GC,
enabling more predictable pause times for real-time use.
GC.malloc_threshold triggers GC based on allocation bytes,
addressing memory pressure from large buffers.
Both default to 0 (disabled), preserving existing behavior.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:23 +09:00
Yukihiro "Matz" Matsumoto 9a736d6609 mruby-bin-mrb: remove mruby-compiler dependency from runtime executor
mrb_ccontext functions are implemented in mruby-compiler (y.tab.c),
causing linker errors when the compiler is excluded from the build.
Replace with mrb_load_irep_file() which is in core (src/load.c).
Also fix incorrect *argv in error message and remove dead fname field.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:23 +09:00
Yukihiro "Matz" Matsumoto eb8c177824 mruby-bin-mrb: add compiler-free runtime executor gem
The `mrb` command executes only precompiled RiteBinary (.mrb) files
without depending on mruby-compiler. This enables smaller binaries
for embedded deployments where scripts are precompiled on a
development machine.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:22 +09:00
Yukihiro "Matz" Matsumoto cfa422b872 gc.c: mark leaf objects directly without gray stack
Leaf types (String, Integer, BigInt, Complex, CPTR) have no children
besides their class pointer. Mark them black immediately in
mrb_gc_mark() instead of pushing to the gray stack, reducing gray
stack pressure and overflow frequency.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:22 +09:00
Yukihiro "Matz" Matsumoto 60cde305c1 gc.c: implement GC.stat method
Return a Hash with GC statistics: live, threshold, state,
generational, full. When MRB_GC_STATS is defined, also includes
total, minor, major counters.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:22 +09:00
Yukihiro "Matz" Matsumoto 52fb294172 gc.c: add optional GC statistics counters
Add gc_total_count, minor_gc_count, major_gc_count (uint32_t) to
mrb_gc, guarded by MRB_GC_STATS. Zero cost when disabled.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:22 +09:00
Yukihiro "Matz" Matsumoto 78658d67e1 vm.c: extract OP_GETIDX, OP_GETIDX0, OP_SETIDX, OP_DIV into static helpers
Phase 2 of opcode handler extraction. these opcodes use
L_SEND_SYM/L_SENDB_SYM fallback for generic method dispatch when
the fast path (Array/Hash/String/Integer/Float) does not apply.
add VM_SEND_SYM and VM_SENDB_SYM return codes. move TYPES2 macro
to file scope for use by vm_op_div.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:22 +09:00
Yukihiro "Matz" Matsumoto 95bfa86160 vm.c: extract OP_ENTER, OP_ARGARY, OP_BLKPUSH into static helpers
extract the three largest self-contained opcode handlers from the
mrb_vm_exec() dispatch loop into static functions. add
__attribute__((flatten)) to mrb_vm_exec() so that the compiler
inlines them back, producing identical binary output while keeping
the source clean.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:22 +09:00
Yukihiro "Matz" Matsumoto 5148062516 mruby-env: add README.md
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:21 +09:00
Yukihiro "Matz" Matsumoto 3272955bf1 mruby-env: add ENV object for environment variable access
ENV is a plain Object with singleton methods and Enumerable,
matching CRuby's behavior. C methods wrap getenv/setenv/unsetenv
with platform support for POSIX, macOS, and Windows.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:21 +09:00
Yukihiro "Matz" Matsumoto 3d53864991 fp_uscale.c: add shortest representation for Float#to_s
Use uscale-based shortest() to compute the minimal decimal string
that uniquely identifies each double. This guarantees perfect
round-trip (parse(to_s(x)) == x) while keeping output concise
(e.g. 0.1 prints as "0.1", not "0.10000000000000001").

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:21 +09:00
Yukihiro "Matz" Matsumoto 9ff1aa9d55 fp_uscale.c: replace fmt_fp.c and readfloat.c with uscale algorithm
Replace separate float formatting (fmt_fp.c) and parsing (readfloat.c)
implementations with a unified fp_uscale.c using 128-bit unrounded
scaling. Both mrb_format_float() and mrb_read_float() now share a
single pow10 table and uscale() primitive for decimal/binary conversion.

This fixes subnormal parsing accuracy (old code returned 0.0 for the
smallest subnormals) and corrects %.2f rounding for values like
12345.125. Table size grows from ~5KB to ~11KB in .rodata.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:21 +09:00
Yukihiro "Matz" Matsumoto 50bc8c6136 vm.c: replace constant cache generation counter with direct invalidation
Remove the per-entry generation field and per-state generation
counter. Invalidation now clears entries directly, removing one
comparison from every OP_GETCONST hot path.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:21 +09:00
Yukihiro "Matz" Matsumoto d0d2c3072c class.c: use 2-way set-associative method cache
Right-shift class pointer by 4 before hashing to remove
always-zero alignment bits, improving hash distribution.
Organize 256 cache entries as 128 sets x 2 ways to reduce
conflict misses when multiple methods share a hash bucket.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:21 +09:00
Yukihiro "Matz" Matsumoto 7675601b92 vm.c: add constant lookup cache with generation counter
Cache OP_GETCONST results in a global direct-mapped cache (64 entries)
keyed by (irep, sym). Invalidate all entries via a generation counter
bumped on mrb_const_set(), mrb_const_remove(), and
mrb_define_const_id(). ~10% faster on constant-heavy code; disabled
with MRB_NO_CONST_CACHE.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:20 +09:00
Yukihiro "Matz" Matsumoto b2d935b6d3 Merge pull request #6803 from saeki-mototsune/fix_psp_build_config 2026-04-23 19:24:10 +09:00
Yukihiro "Matz" Matsumoto 36dd668f73 Merge pull request #6799 from dearblue/mrb_state 2026-04-23 19:20:43 +09:00
Yukihiro "Matz" Matsumoto 8996152365 Merge pull request #6786 from dearblue/sysfail 2026-04-23 19:15:38 +09:00
Yukihiro "Matz" Matsumoto 7365ed526b Merge pull request #6785 from khasinski/cfunc-proc-aspec 2026-04-23 19:11:35 +09:00
Yukihiro "Matz" Matsumoto 7a14bad5ea Merge pull request #6777 from dearblue/array-combination.4 2026-04-23 18:59:05 +09:00
Yukihiro "Matz" Matsumoto bfc1f37b91 Merge pull request #6775 from dearblue/array-combination.2 2026-04-23 18:56:46 +09:00
Yukihiro "Matz" Matsumoto 7eab8302b0 Merge pull request #6774 from dearblue/array-combination.1 2026-04-23 18:49:28 +09:00
Yukihiro "Matz" Matsumoto 05f7236586 Merge pull request #6805 from mruby/dependabot/github_actions/github-actions-dependencies-f3e34333ea 2026-04-23 16:01:46 +09:00
Yukihiro "Matz" Matsumoto d359182b47 Merge pull request #6804 from mruby/dependabot/bundler/bundler-dependencies-39953ac9c0 2026-04-23 15:52:01 +09:00
dependabot[bot] ccd661b429 build(deps): bump github/codeql-action
Bumps the github-actions-dependencies group with 1 update: [github/codeql-action](https://github.com/github/codeql-action).


Updates `github/codeql-action` from 4.35.1 to 4.35.2
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v4.35.1...v4.35.2)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.35.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-22 14:54:14 +00:00
dependabot[bot] aea5d584f0 build(deps): bump rake in the bundler-dependencies group
Bumps the bundler-dependencies group with 1 update: [rake](https://github.com/ruby/rake).


Updates `rake` from 13.3.1 to 13.4.1
- [Release notes](https://github.com/ruby/rake/releases)
- [Changelog](https://github.com/ruby/rake/blob/master/History.rdoc)
- [Commits](https://github.com/ruby/rake/compare/v13.3.1...v13.4.1)

---
updated-dependencies:
- dependency-name: rake
  dependency-version: 13.4.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: bundler-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-21 14:53:28 +00:00
SaekiMototsune 08b6d2ecd0 Disable some gems on build_config for playstationportable 2026-04-21 18:50:14 +09:00
Yukihiro "Matz" Matsumoto f469e7567a Merge pull request #6801 from mruby/dependabot/github_actions/github-actions-dependencies-9527efa922 2026-04-21 08:34:06 +09:00
dependabot[bot] 86940418c7 build(deps): bump the github-actions-dependencies group with 2 updates
Bumps the github-actions-dependencies group with 2 updates: [actions/cache](https://github.com/actions/cache) and [j178/prek-action](https://github.com/j178/prek-action).


Updates `actions/cache` from 5.0.4 to 5.0.5
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/668228422ae6a00e4ad889ee87cd7109ec5666a7...27d5ce7f107fe9357f9df03efb73ab90386fccae)

Updates `j178/prek-action` from 2.0.1 to 2.0.2
- [Release notes](https://github.com/j178/prek-action/releases)
- [Commits](https://github.com/j178/prek-action/compare/53276d8b0d10f8b6672aa85b4588c6921d0370cc...cbc2f23eb5539cf20d82d1aabd0d0ecbcc56f4e3)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: 5.0.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions-dependencies
- dependency-name: j178/prek-action
  dependency-version: 2.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-20 17:20:54 +00:00
Yukihiro "Matz" Matsumoto 7546d53a3a Merge pull request #6800 from mruby/stable 2026-04-20 18:54:55 +09:00
dearblue 0d00743a5f Define the typedef for mrb_state earlier
This improves consistency with other definitions.
2026-04-19 21:17:18 +09:00
Yukihiro "Matz" Matsumoto 0112fe6659 Merge pull request #6796 from mruby/dependabot/pre_commit/pre-commit-hooks-be5b5e25f5 2026-04-18 08:29:15 +09:00
Yukihiro "Matz" Matsumoto 605ef4919b Merge pull request #6797 from mruby/dependabot/github_actions/github-actions-dependencies-fd00acb19b 2026-04-18 08:28:56 +09:00
Yukihiro "Matz" Matsumoto e1ed206cae Merge pull request #6798 from mruby/dependabot/bundler/yard-0.9.42 2026-04-18 08:28:31 +09:00
Yukihiro "Matz" Matsumoto 82b91d15c5 Merge pull request #6794 from mattn/fix/readfloat-18-digits 2026-04-18 08:27:58 +09:00
dependabot[bot] f80f825bcf build(deps): bump yard from 0.9.38 to 0.9.42
Bumps [yard](https://yardoc.org) from 0.9.38 to 0.9.42.

---
updated-dependencies:
- dependency-name: yard
  dependency-version: 0.9.42
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-17 22:33:28 +00:00
dependabot[bot] eb51c63196 build(deps): bump github/codeql-action
Bumps the github-actions-dependencies group with 1 update: [github/codeql-action](https://github.com/github/codeql-action).


Updates `github/codeql-action` from 4 to 4.35.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v4...v4.35.1)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.35.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-17 14:53:59 +00:00
dependabot[bot] c2145c8642 build(deps): bump https://github.com/rubocop/rubocop
Bumps the pre-commit-hooks group with 1 update: [https://github.com/rubocop/rubocop](https://github.com/rubocop/rubocop).


Updates `https://github.com/rubocop/rubocop` from v1.86.0 to 1.86.1
- [Release notes](https://github.com/rubocop/rubocop/releases)
- [Changelog](https://github.com/rubocop/rubocop/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rubocop/rubocop/compare/v1.86.0...v1.86.1)

---
updated-dependencies:
- dependency-name: https://github.com/rubocop/rubocop
  dependency-version: 1.86.1
  dependency-type: direct:production
  dependency-group: pre-commit-hooks
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-16 14:57:37 +00:00
Yasuhiro Matsumoto b82f1c2ec0 readfloat.c: keep one extra fraction digit (17 -> 18)
18 decimal digits still fit in uint64_t (max ~1.8e19), so we can hold
one more digit of precision without overflow risk.
2026-04-16 17:13:17 +09:00
Yukihiro "Matz" Matsumoto a6a92bf95b Merge pull request #6793 from mattn/fix/readfloat-correctly-rounded 2026-04-16 17:09:10 +09:00
Yasuhiro Matsumoto 10c9e83128 readfloat.c: correctly round fraction via division by exact 10^n
The previous code computed `frac_part * pow10_negative[n]`, where
pow10_negative[n] is already a rounded approximation of 10^-n (since
10^-n is not exactly representable in binary). The multiplication then
adds another rounding step, leaving up to ~1 ulp of error.

Dividing `frac_part` by `pow10_positive[n]` is exact for n <= 22 (the
range where 10^n fits exactly in a double), so the division is the
only rounding and the result is correctly rounded. For example,
"0.3".to_f now matches the 0.3 literal's bit pattern (and libc strtod).
2026-04-16 14:47:14 +09:00
Yukihiro "Matz" Matsumoto 2f5a24ed9b Merge pull request #6792 from mruby/fix/host-cxx-build-dir 2026-04-15 15:34:09 +09:00
Yukihiro "Matz" Matsumoto 984f23a94e Merge pull request #6788 from mruby/dependabot/github_actions/github-actions-dependencies-d2d9db5a03 2026-04-15 09:49:36 +09:00
Yukihiro "Matz" Matsumoto 2575735152 Merge pull request #6791 from mruby/fix/bigint-cxx-compat 2026-04-15 09:33:59 +09:00
Yukihiro "Matz" Matsumoto eb87d59941 Merge pull request #6790 from mruby/fix/mrb-mt-entry-cxx-compat 2026-04-15 09:33:23 +09:00
Yukihiro "Matz" Matsumoto 8b44a52176 host-cxx: use distinct build directory from host-debug
build_config/host-cxx.rb defaulted to MRuby::Build.new (no name),
which is equivalent to MRuby::Build.new('host'), so it shared
build/host/ with build_config/host-debug.rb.

The two configs produce ABI-incompatible object files (C linkage
vs. C++-mangled).  Switching between configs without a clean step
silently mixes stale objects, leading to confusing undefined
reference errors at link time.

Name the C++ build 'host-cxx' so it gets its own build/host-cxx/
directory.  No external paths reference build/host/ in a way that
this change would break.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-15 09:30:33 +09:00
Yukihiro "Matz" Matsumoto 617a55b775 mruby-bigint: avoid C99 compound literal in MPZ_CTX_INIT
MPZ_CTX_INIT used a compound literal with designated initializers:

  mpz_ctx_t ctx##_struct = ((mpz_ctx_t){.mrb = ..., .pool = ...});

Both features are C99-only, and are not accepted by legacy C++
compilers (notably gcc 4.x) when mruby is pulled into a C++
translation unit via the -cxx.cxx wrapper.

Replace with plain member assignment so the macro expands to code
that is valid under C89/C++98 as well.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-15 09:01:19 +09:00
Yukihiro "Matz" Matsumoto a33ccd67b1 class.h: avoid C99 designated initializers in MRB_MT_ENTRY
Older C++ compilers (notably gcc 4.x) do not support C99 struct field
designators (.func = ...) even as an extension, which blocks building
mruby when it is included from a C++ translation unit under such
toolchains.

Reorder union mrb_mt_ptr so that mrb_func_t is the first member, and
switch MRB_MT_ENTRY to positional aggregate initialization.  Both are
compatible with pre-C99 / pre-C++20 compilers.

Fixes #6789

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-15 07:31:11 +09:00
dependabot[bot] c43eba8979 build(deps): bump softprops/action-gh-release
Bumps the github-actions-dependencies group with 1 update: [softprops/action-gh-release](https://github.com/softprops/action-gh-release).


Updates `softprops/action-gh-release` from 2 to 2.6.1
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/v2...v3)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-version: 2.6.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-13 16:11:02 +00:00
dearblue 6c06b4cd9d Early conversion of mesg to a string object in mrb_sys_fail()
`mrb_class_get_id()` may call the `#const_missing` method.
Therefore, if the `mesg` string originates from a string object, it may reference an invalid address.

And since `errno` might also change during the call to `#const_missing`, save this as well beforehand.

Also, while `mrb_class_defined_id()` does not currently call the `#const_defined?` method, it is unclear whether this will remain the case in the future.
2026-04-13 23:26:15 +09:00
dearblue 9ad94a50e7 Supplement to #6781
`mrb_str_dup()` always duplicates string objects in an unfrozen state, and the class is also set.
Therefore, it can be observed and modified from the Ruby side using the `ObjectSpace.each_object` method.

By using `mrb_str_dup_frozen()`, unnecessary duplication can be avoided, and modifications to the string can also be prevented.
2026-04-13 21:49:59 +09:00
dearblue d8e0d24fea Add mrb_str_dup_frozen() 2026-04-13 21:49:59 +09:00
Chris Hasiński a6283dbdf3 Clear existing aspec bits before setting new ones in mrb_proc_set_cfunc_aspec 2026-04-13 13:40:35 +02:00
Chris Hasiński 55f0228bf8 Store compressed aspec on cfunc RProc for correct arity/parameters
Compress the 24-bit aspec into 13 free flag bits on RProc (bits 0-6
and 14-19) when wrapping cfunc methods. Field widths: req/opt 3 bits
(max 7), post/key 2 bits (max 3), rest/kdict/block 1 bit each. Values
exceeding the compressed range are clamped and rest is forced to 1.

This enables Proc#arity and Proc#parameters to return correct results
for cfunc-backed Procs (e.g. from Method#to_proc) with zero memory
overhead -- no struct change needed.

Closes #6764
2026-04-13 13:11:27 +02:00
dearblue d246ac6c8a Make Array#__combination_next return an array of elements
Since the main processing will be completed on the C side, the Ruby side will simply call the block.
2026-04-08 22:29:56 +09:00
dearblue 5e3e982442 Improve the calculation of the next index for Array#__combination_next
When the index wraps around, the lower index becomes a fixed value.
2026-04-08 22:16:59 +09:00
dearblue 3a9ef9d27e Avoid using the deprecated function mrb_data_check_and_get()
The "d" directive in `mrb_get_args()` can be used as an alternative.
Furthermore, NULL checking is unnecessary for the following reasons:
  - Incomplete objects from `ary_combination_init()` are not passed to the caller and are garbage collected when `ObjectSpace.each_object` is called, so they are never retrieved
  - Even if `state.clone` is called, the `RData::type` of the cloned object is set to NULL, so it is rejected by `mrb_get_args()`
2026-04-08 21:10:45 +09:00
John Bampton 95b74eefdf pre-commit: add hook to ensure Makefiles are indented with tabs
Manual hook added which runs a shell script.

Can be run locally manually by Linux or Mac users and we have this also running on the GitHub Actions CI.

Will fail if Makefiles uses spaces for indentation.

Can run the manual hooks with:

`pre-commit run check-makefile-indentation --all-files --hook-stage manual`
2025-12-30 03:30:04 +10:00
John Bampton be3212906a pre-commit add official meta hook check-useless-excludes 2025-07-29 13:19:06 +10:00
dearblue 40371ebf01 Generate the string object last at the end of processing
This is simpler than processing `suffix` after the string object has been created.
2025-07-21 10:42:31 +09:00
dearblue d230bb16d4 Use end pointers instead of lengths
For processing simplicity.
2025-07-21 10:42:31 +09:00
dearblue 3f27133639 Take a const char * for the "z" specifier of mrb_get_args() 2025-07-21 10:39:11 +09:00
dearblue f72a32ad87 Improvements to the Windows implementation of File.basename
This change is mainly to remove `_splitpath()`.

The reason is:

  - UNC paths are not supported.
  - The behavior is not guaranteed to be consistent when passing a UTF-8 string, since the handling of multibyte characters changes depending on the set codepage.
  - Only up to 255 bytes can be processed.

Therefore, it is preferable to integrate with non-Windows implementations to ensure consistent implementation behavior.
2025-07-21 10:39:11 +09:00
dearblue 1f7bc64845 Added test for File.basename 2025-07-21 10:16:38 +09:00
dearblue 627452e340 Sharing arrays with ary_dup()
Until now, the array entity has always been allocated from the heap and copied.
However, this allows the array entity to be shared with the source if possible.
2025-07-18 23:01:28 +09:00
dearblue 3c653fc0d4 Share array entities if possible with ary.replace(frozen_ary)
Currently mruby is limited to always placing array entities on the rewritable heap.
Therefore, if the original array was frozen and only rewritable objects survived the subsequent process, the array entity can be changed.
If an array object that actually shared the array entity is frozen with `ary.freeze`, there should be no problem, since the shared state is still kept.
2025-07-18 22:51:00 +09:00
dearblue b667b052dd Remove iterating variable from mrb_objspace_each_objects() 2024-08-25 20:47:26 +09:00
dearblue ef763285e9 Reduced description of mrb_init_core()
Since `mrb_init_math()` does not exist, it is no longer declared as a prototype.
2024-03-27 22:24:42 +09:00
take_cheeze 0bd391bc6f Fix return type of symbol slicing.
Thanks to @suzukake .
2014-07-28 16:30:14 +09:00
take_cheeze 2249cf7c7f Implement Symbol#slice and Symbol#[] in mruby-symbol-ext. 2014-07-28 16:30:14 +09:00
222 changed files with 18794 additions and 5650 deletions
+1 -1
View File
@@ -52,7 +52,7 @@ jobs:
- name: Ruby version
run: ruby -v
- name: Cache cosmocc
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
id: cache-cosmocc
with:
path: ~/cosmo
+3 -3
View File
@@ -22,12 +22,12 @@ jobs:
with:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v4
uses: github/codeql-action/autobuild@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
with:
category: "Security"
+1 -1
View File
@@ -9,7 +9,7 @@ jobs:
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
- uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6.1.0
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
sync-labels: true
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: j178/prek-action@53276d8b0d10f8b6672aa85b4588c6921d0370cc # v2.0.1
- uses: j178/prek-action@bdca6f102f98e2b4c7029491a53dfd366469e33d # v2.0.4
with:
install-only: true
- name: Run manual pre-commit hooks
+1 -1
View File
@@ -15,6 +15,6 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: j178/prek-action@53276d8b0d10f8b6672aa85b4588c6921d0370cc # v2.0.1
- uses: j178/prek-action@bdca6f102f98e2b4c7029491a53dfd366469e33d # v2.0.4
with:
extra-args: --all-files
+1 -1
View File
@@ -40,7 +40,7 @@ jobs:
mv .sha256 "$packagename.sha256"
)
- name: Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
with:
draft: true
prerelease: ${{ contains(github.ref_name, '-rc') }}
+14 -2
View File
@@ -15,6 +15,9 @@ repos:
- id: check-hooks-apply
name: run check-hooks-apply
description: check hooks apply to the repository
- id: check-useless-excludes
name: run check-useless-excludes
description: clean up unnecessary exclusion patterns
- repo: local
hooks:
- id: prettier
@@ -26,6 +29,15 @@ repos:
additional_dependencies: ["prettier@3.7.4"]
pass_filenames: false
stages: [manual]
- id: check-makefile-indentation
name: check Makefiles are indented with tabs
description: ensures that Makefiles are indented with tabs
entry: ./scripts/check_makefiles_for_tabs.sh
language: system
files: "(?i)^makefile$"
pass_filenames: true # <-- Crucial change: pass filenames to the script
types: [file] # Ensure only regular files are passed, not directories
stages: [manual]
- id: check-zip-file-is-not-committed
name: disallow zip files
description: Zip files are not allowed in the repository
@@ -41,7 +53,7 @@ repos:
name: run gitleaks
description: detect hardcoded secrets with gitleaks
- repo: https://github.com/oxipng/oxipng
rev: v10.1.0
rev: v10.1.1
hooks:
- id: oxipng
name: run oxipng
@@ -106,7 +118,7 @@ repos:
types: [markdown]
files: \.md$
- repo: https://github.com/rubocop/rubocop
rev: v1.86.0
rev: v1.86.2
hooks:
- id: rubocop
name: run rubocop
+16 -12
View File
@@ -1,25 +1,25 @@
# Authors of mruby (mruby developers)
## The List of Contributors sorted by number of commits (as of 2026-03-02 02877f0)
## The List of Contributors sorted by number of commits (as of 2026-05-31 9d084b0)
7532 Yukihiro "Matz" Matsumoto (@matz)*
712 dearblue (@dearblue)*
7747 Yukihiro "Matz" Matsumoto (@matz)*
749 dearblue (@dearblue)*
587 KOBAYASHI Shuji (@shuujii)
353 Daniel Bovensiepen (@bovi)*
345 Takeshi Watanabe (@take-cheeze)*
333 Masaki Muranaka (@monaka)
255 John Bampton (@jbampton)
266 John Bampton (@jbampton)
234 Jun Hiroe (@suzukaze)
228 Tomoyuki Sahara (@tsahara)*
220 Cremno (@cremno)*
209 Yuki Kurihara (@ksss)+
144 Yasuhiro Matsumoto (@mattn)*
146 Yasuhiro Matsumoto (@mattn)*
113 Carson McDonald (@carsonmcdonald)
104 Tomasz Pędraszewski (@dabroz)*
83 Akira Yumiyama (@akiray03)*
83 skandhas (@skandhas)
80 Masamitsu MURASE (@masamitsu-murase)
73 Hiroshi Mimaki (@mimaki)*
79 Hiroshi Mimaki (@mimaki)*
71 Tatsuhiko Kubo (@cubicdaiya)*
71 Yuichiro MASUI (@masuidrive)
62 Yuichiro Kaneko (@yui-knk)+
@@ -36,8 +36,10 @@
32 Masayoshi Takahashi (@takahashim)+
31 MATSUMOTO Ryosuke (@matsumotory)*
30 Nobuyoshi Nakada (@nobu)
29 HASUMI Hitoshi (@hasumikin)
26 Hoshiumi Arata (@hoshiumiarata)*
25 Julian Aron Prenner (@furunkel)*
23 leviongit (@leviongit)
22 Clayton Smith (@clayton-shopify)
22 Uchio Kondo (@udzura)*
22 Zachary Scott (@zzak)*
@@ -50,10 +52,9 @@
18 Corey Powell (@IceDragon200)
18 Hidetaka Takano (@TJ-Hidetaka-Takano)
18 Jon Maken (@jonforums)+
18 leviongit (@leviongit)
18 mirichi (@mirichi)
17 Mitchell Blank Jr (@mitchblank)*
16 HASUMI Hitoshi (@hasumikin)
16 Hendrik (@Asmod4n)
16 bggd (@bggd)
16 kano4 (@kano4)
15 Felix Jones (@felixjones)*
@@ -76,7 +77,7 @@
11 RIZAL Reckordp (@Reckordp)+
11 Seeker (@SeekingMeaning)
11 takkaw (@takkaw)
10 Hendrik (@Asmod4n)
10 Chris Hasiński (@khasinski)
10 Miura Hideki (@miura1729)
10 Narihiro Nakamura (@authorNari)
10 YAMAMOTO Masaya (pandax381)
@@ -88,6 +89,7 @@
8 Wataru Ashihara (@wataash)*
7 Bhargava Shastry (@bshastry)*
7 Kouichi Nakanishi (@keizo042)
7 Paweł Świątkowski (@katafrakt)
7 Rubyist (@expeditiousRubyist)
7 Simon Génier (@simon-shopify)
7 Terence Lee (@hone)
@@ -101,7 +103,6 @@
6 INOUE Yasuyuki (@yasuyuki)
6 Junji Sawada (@junjis0203)
6 Kenji Okimoto (@okkez)+
6 Paweł Świątkowski (@katafrakt)
6 Selman ULUG (@selman)
6 Yusuke Endoh (@mame)*
6 buty4649 (@buty4649)
@@ -120,7 +121,6 @@
5 dreamedge (@dreamedge)
5 nkshigeru (@nkshigeru)
5 xuejianqing (@joans321)
4 Chris Hasiński (@khasinski)
4 Dante Catalfamo (@dantecatalfamo)
4 Goro Kikuchi (@gorogit)
4 Herwin Weststrate (@herwinw)
@@ -140,6 +140,7 @@
4 Yuji Yamano (@yyamano)
4 kurodash (@kurodash)*
4 wanabe (@wanabe)*
2 0x1eef (@0x1eef)
3 Anton Davydov (@davydovanton)
3 Aurora Nockert (@auroranockert)
3 Carlo Prelz (@asfluido)*
@@ -192,6 +193,7 @@
2 Masahiro Wakame (@vvkame)+
2 Minao Yamamoto (@tarosay)+
2 Nihad Abbasov (@NARKOZ)
2 Pete Kinnecom (@petekinnecom)
2 Robert Mosolgo (@rmosolgo)
2 Russel Hunter Yukawa (@rhykw)+
2 Ryunosuke SATO (@tricknotes)
@@ -220,6 +222,7 @@
1 Colin MacKenzie IV (@sinisterchipmunk)
1 Daehyub Kim (@lateau)
1 Daniel Varga (@vargad)
1 David Korczynski (@DavidKorczynski)
1 Diamond Rivero (@diamant3)
1 Edgar Boda-Majer (@eboda)
1 Fangrui Song (@MaskRay)
@@ -274,7 +277,6 @@
1 Patrick Ellis (@pje)
1 Patrick Pokatilo (@SHyx0rmZ)
1 Pavel Evstigneev (@Paxa)+
1 Pete Kinnecom (@petekinnecom)
1 Piotr Usewicz (@pusewicz)
1 Prayag Verma (@pra85)
1 Ranmocy (@ranmocy)
@@ -282,6 +284,7 @@
1 Ryan Scott Lewis (@RyanScottLewis)
1 Ryo Okubo (@syucream)
1 SAkira a.k.a. Akira Suzuki (@sakisakira)
1 SaekiMototsune (@saeki-mototsune)
1 Santiago Rodriguez (@sanrodari)
1 Satoh, Hiroh (@cho45)+
1 Satoru Naba (@snaba)+
@@ -325,6 +328,7 @@
1 sbsoftware (@sbsoftware)
1 ssmallkirby (@smallkirby)
1 taku toyama (@tsuichu)
1 vobloeb (@vobloeb)
`*` - Entries unified according to names and addresses
`+` - Entries with names different from commits
+2 -2
View File
@@ -2,8 +2,8 @@ GEM
remote: https://rubygems.org/
specs:
coderay (1.1.3)
rake (13.3.1)
yard (0.9.38)
rake (13.4.2)
yard (0.9.44)
yard-coderay (0.1.0)
coderay
yard
+2 -4
View File
@@ -292,9 +292,7 @@ class Scene
r = rad.x / nsfs
g = rad.y / nsfs
b = rad.z / nsfs
printf("%c", clamp(r))
printf("%c", clamp(g))
printf("%c", clamp(b))
print([clamp(r), clamp(g), clamp(b)].pack("CCC"))
end
end
end
@@ -303,7 +301,7 @@ end
# File.open("ao.ppm", "w") do |fp|
printf("P6\n")
printf("%d %d\n", IMAGE_WIDTH, IMAGE_HEIGHT)
printf("255\n", IMAGE_WIDTH, IMAGE_HEIGHT)
printf("255\n")
Scene.new.render(IMAGE_WIDTH, IMAGE_HEIGHT, NSUBSAMPLES)
# Scene.new.render(256, 256, 2)
# end
+2 -4
View File
@@ -64,10 +64,8 @@ MRuby::Build.new do |conf|
# APE binaries use .com extension
conf.exts.executable = '.com'
# Cosmopolitan provides POSIX compatibility, explicitly select POSIX HALs
conf.gem core: 'hal-posix-io'
conf.gem core: 'hal-posix-socket'
conf.gem core: 'hal-posix-dir'
# Cosmopolitan provides POSIX compatibility
conf.ports :posix
# Standard library
conf.gembox 'stdlib'
+88
View File
@@ -0,0 +1,88 @@
MRuby::Build.new do |conf|
# load specific toolchain settings
conf.toolchain
# Use mrbgems
# conf.gem 'examples/mrbgems/ruby_extension_example'
# conf.gem 'examples/mrbgems/c_extension_example' do |g|
# g.cc.flags << '-g' # append cflags in this gem
# end
# conf.gem 'examples/mrbgems/c_and_ruby_extension_example'
# conf.gem :core => 'mruby-eval'
# conf.gem :mgem => 'mruby-onig-regexp'
# conf.gem :github => 'mattn/mruby-onig-regexp'
# conf.gem :git => 'git@github.com:mattn/mruby-onig-regexp.git', :branch => 'master', :options => '-v'
# include the GEM box
#conf.gembox 'default'
# C compiler settings
# conf.cc do |cc|
# cc.command = ENV['CC'] || 'gcc'
# cc.flags = [ENV['CFLAGS'] || %w()]
# cc.include_paths = ["#{root}/include"]
# cc.defines = %w()
# cc.option_include_path = %q[-I"%s"]
# cc.option_define = '-D%s'
# cc.compile_options = %Q[%{flags} -MMD -o "%{outfile}" -c "%{infile}"]
# end
# mrbc settings
# conf.mrbc do |mrbc|
# mrbc.compile_options = "-g -B%{funcname} -o-" # The -g option is required for line numbers
# end
# Linker settings
# conf.linker do |linker|
# linker.command = ENV['LD'] || 'gcc'
# linker.flags = [ENV['LDFLAGS'] || []]
# linker.flags_before_libraries = []
# linker.libraries = %w()
# linker.flags_after_libraries = []
# linker.library_paths = []
# linker.option_library = '-l%s'
# linker.option_library_path = '-L%s'
# linker.link_options = %Q[%{flags} -o "%{outfile}" %{objs} %{libs}]
# end
# Archiver settings
# conf.archiver do |archiver|
# archiver.command = ENV['AR'] || 'ar'
# archiver.archive_options = 'rs "%{outfile}" %{objs}'
# end
# Parser generator settings
# conf.yacc do |yacc|
# yacc.command = ENV['YACC'] || 'bison'
# yacc.compile_options = %q[-o "%{outfile}" "%{infile}"]
# end
# gperf settings
# conf.gperf do |gperf|
# gperf.command = 'gperf'
# gperf.compile_options = %q[-L ANSI-C -C -j1 -i 1 -o -t -N mrb_reserved_word -k"1,3,$" "%{infile}" > "%{outfile}"]
# end
# file extensions
# conf.exts do |exts|
# exts.object = '.o'
# exts.executable = '' # '.exe' if Windows
# exts.library = '.a'
# end
# file separator
# conf.file_separator = '/'
# change library directory name from the default "lib" if necessary
# conf.libdir_name = 'lib64'
# Turn on `enable_debug` for better debugging
conf.enable_sanitizer 'address,undefined'
conf.enable_debug
conf.enable_bintest
conf.enable_test
conf.ports :glib
conf.cc.defines << 'MRB_TASK_BUILD_DEMO'
conf.gem core: 'mruby-task'
conf.gem core: 'mruby-compiler'
end
+1 -1
View File
@@ -1,4 +1,4 @@
MRuby::Build.new do |conf|
MRuby::Build.new('host-cxx') do |conf|
conf.toolchain
# include the default GEMs
+2
View File
@@ -13,6 +13,8 @@ MRuby::Build.new('host') do |conf|
# Generate mruby debugger command (require mruby-eval)
conf.gem :core => "mruby-bin-debugger"
# Regexp is included via stdlib.gembox
# test
conf.enable_test
# bintest
+78 -20
View File
@@ -1,36 +1,94 @@
# NOTE: Currently, this configuration file does not support VisualC++!
# Your help is needed!
# Build mruby with a shared libmruby.so (in addition to the usual
# libmruby.a / executables).
#
# Produces (in build/host/lib/):
# libmruby.a the static archive (as in the default build)
# libmruby.so the shared library, with SONAME=libmruby.so.<MAJOR>.<MINOR>
# libmruby.so.<MAJOR>.<MINOR> symlink to libmruby.so (matches SONAME)
# libmruby.map linker version script (MRUBY_<RELEASE_NO>)
#
# Also produces the matching libmruby_core.so + symlink for completeness.
#
# Symbol versioning ties to MRUBY_RELEASE_NO (e.g. MRUBY_40000 for 4.0.0).
# mruby has historically had ABI breaks between TEENY versions, so the
# version tag uses the full release number rather than just MAJOR.MINOR.
#
# The shared library is built FROM the static archive via
# `-Wl,--whole-archive`, so the existing static-build pipeline (including
# the test infrastructure) is unaffected. Executables in build/host/bin
# remain statically linked; distros that want dynamically-linked
# executables can rebuild them against the .so.
#
# NOTE: gcc/clang only — VisualC++ support requires a separate config.
require "mruby/source"
MRuby::Build.new do |conf|
# load specific toolchain settings
conf.toolchain
# include the GEM box
conf.gembox 'default'
# C compiler settings
# -fPIC so the static archive's contents can be linked into the .so.
conf.compilers.each do |cc|
cc.flags << '-fPIC'
end
conf.archiver do |archiver|
archiver.command = cc.command
archiver.archive_options = '-shared -o %{outfile} %{objs}'
end
# file extensions
conf.exts do |exts|
exts.library = '.so'
end
# file separator
# conf.file_separator = '/'
# enable this if better compatibility with C++ is desired
#conf.enable_cxx_exception
# Turn on `enable_debug` for better debugging
conf.enable_debug
conf.enable_bintest
conf.enable_test
end
# Add the shared-library targets as a post-build pass, so the default
# static-build pipeline remains untouched.
MRuby.each_target do
next unless name == "host"
libdir = File.join(build_dir, libdir_name)
vermap = File.join(build_dir, "libmruby.map")
vertag = "MRUBY_#{MRuby::Source::MRUBY_RELEASE_NO}"
# Generate the version script eagerly — it has no .o dependencies and is
# tiny enough that lazy generation isn't worth the rake plumbing.
mkdir_p File.dirname(vermap)
File.write(vermap, <<~MAP)
#{vertag} {
global: *;
local: *;
};
MAP
major = MRuby::Source::MRUBY_RELEASE_MAJOR
minor = MRuby::Source::MRUBY_RELEASE_MINOR
[
[libmruby_static, "libmruby"],
[libmruby_core_static, "libmruby_core"],
].each do |archive, basename|
so = File.join(libdir, "#{basename}.so")
symlink = "#{so}.#{major}.#{minor}"
soname = "#{basename}.so.#{major}.#{minor}"
# Build .so from the static archive via --whole-archive.
file so => [archive, vermap] do |t|
_pp "LD", so.relative_path
sh "#{cc.command} -shared -fPIC -o #{so}" \
" -Wl,-soname,#{soname}" \
" -Wl,--version-script=#{vermap}" \
" -Wl,--whole-archive #{archive} -Wl,--no-whole-archive" \
" -lm"
end
products << so
# SONAME-matching symlink: needed so executables linked with -lmruby
# (which embeds the SONAME as DT_NEEDED) can find the library at
# runtime via standard search paths.
file symlink => so do |t|
_pp "LN", "#{symlink.relative_path} -> #{File.basename(so)}"
rm_f symlink
File.symlink(File.basename(so), symlink)
end
products << symlink
end
end
+5 -3
View File
@@ -72,7 +72,9 @@ MRuby::CrossBuild.new("playstationportable") do |conf|
conf.gem :core => "mruby-os-memsize"
conf.gem :core => "mruby-proc-binding"
conf.gem :core => "mruby-sleep"
conf.gem :core => "mruby-io"
conf.gem :core => "mruby-dir"
#conf.gem :core => "mruby-socket" unsupported
# Disabled until PSP-specific HALs are available; the POSIX HALs depend on
# APIs that the PSP SDK does not fully provide.
# conf.gem :core => "mruby-io"
# conf.gem :core => "mruby-dir"
# conf.gem :core => "mruby-socket"
end
+3 -3
View File
@@ -73,14 +73,14 @@ The following gems work with amalgamation:
- `mruby-enum-ext`, `mruby-compar-ext`
- `mruby-error`, `mruby-math`, `mruby-struct`
- `mruby-bigint`, `mruby-rational`, `mruby-complex`
- `mruby-io` (with `hal-posix-io`)
- `mruby-task` (with `hal-posix-task`)
- `mruby-io` (with the active `ports/<name>/` HAL)
- `mruby-task` (with the active `ports/<name>/` HAL)
### Platform-Dependent Gems
Gems that use a HAL (Hardware Abstraction Layer) include
platform-specific code in the amalgamation. For example, if
`mruby-io` selects `hal-posix-io` on Linux, the generated `mruby.c`
`mruby-io` selects its POSIX port on Linux, the generated `mruby.c`
contains POSIX-specific code and cannot be compiled on Windows.
If you need amalgamated files for multiple platforms, generate them
+7
View File
@@ -381,6 +381,13 @@ mrb_define_method(mrb, point, "initialize", point_init, MRB_ARGS_REQ(2));
mrb_define_method(mrb, point, "x", point_x, MRB_ARGS_NONE());
```
**Do not call into mruby from a `dfree` handler.** The handler runs
from inside GC sweep; allocating Ruby objects, calling
`mrb_funcall`, `mrb_yield`, raising exceptions, or otherwise
re-entering the VM can trigger a recursive GC that revisits the
same object and causes double-free. Keep `dfree` to `mrb_free` /
plain C cleanup of the wrapped data only.
## Exception Handling
### Raising Exceptions
+62
View File
@@ -172,6 +172,8 @@ The maximal GEM structure looks like this:
|
+- src/ <- Source for C extension
|
+- ports/<name>/ <- Platform-specific C sources (see Platform Ports)
|
+- tools/ <- Source for Executable (in C)
|
+- test/ <- Test code (Ruby)
@@ -182,6 +184,8 @@ contains C/C++ files to extend mruby. The `include` directory contains C/C++ hea
files. The `test` directory contains C/C++ and pure Ruby files for testing purposes
which will be used by `mrbtest`. `mrbgem.rake` contains the specification
to compile C and Ruby files. `README.md` is a short description of your GEM.
The optional `ports/<name>/` directories hold platform-specific C sources
selected at build time; see [Platform Ports](#platform-ports-ports) below.
## Build process
@@ -332,6 +336,64 @@ end
**NOTE**: Using the `build_settings` method will cause GEM's all build command settings
directly written in the block passed to `MRuby::Gem::Specification.new` to be ignored.
## Platform Ports (ports/)
A gem may ship platform-specific C sources under `ports/<name>/`
subdirectories. The build configuration selects which port name(s)
are active via `conf.ports`, and each gem compiles the sources of
the first matching `ports/<name>/` it ships:
```ruby
MRuby::Build.new do |conf|
conf.toolchain
conf.ports :posix # selects ports/posix/ across all gems
end
```
`conf.ports` accepts multiple names as a fallback chain. Each gem
picks the first directory in the list that exists on its side:
```ruby
conf.ports :rp2040, :posix # try rp2040 per-gem, else posix
```
Host builds auto-detect `:posix` or `:win` when `conf.ports` is
not set. Sources outside `ports/` (i.e. `src/`) are always
compiled regardless of the port selection.
### External HAL Provider Gems
A third-party gem may replace another gem's bundled port at build
time. A gem whose name matches `hal-<short>-<conf>` is recognized
as the external HAL provider for the target gem whose name's last
`-`-separated segment is `<short>`. For example, `hal-task-glib`
overrides the HAL of `mruby-task`; `hal-io-uring` would override
`mruby-io`. The HAL provider must depend on its target so it can
`#include` the target's HAL header:
```ruby
MRuby::Gem::Specification.new('hal-task-glib') do |spec|
spec.license = 'MIT'
spec.author = 'Your Name'
spec.summary = 'GLib HAL for mruby-task'
spec.add_dependency 'mruby-task', core: 'mruby-task'
# src/ contains the HAL implementation
end
```
When a matching HAL provider gem is present in the build, the
target gem's `ports/<conf.ports>/` sources are dropped from the
build automatically. The HAL provider's own sources supply the
implementation instead, avoiding duplicate symbol errors at link
time. Loading two gems that match the same `hal-<short>-*`
prefix is a build error.
The naming convention is the only signal -- no spec attribute,
no `add_dependency` flag is required. A gem author who wants to
contribute an additional bundled port upstream sends a PR adding
`<target-gem>/ports/<name>/`; a gem author who prefers to ship
out of tree publishes a `hal-<short>-<conf>` gem instead.
## C Extension
mruby can be extended with C. This is possible by using the C API to
+6 -3
View File
@@ -117,8 +117,8 @@ Defined in `include/mruby/class.h`:
```c
union mrb_mt_ptr {
mrb_func_t func; /* first member: see MRB_MT_ENTRY note */
const struct RProc *proc;
mrb_func_t func;
};
typedef struct mrb_mt_entry {
@@ -139,9 +139,12 @@ typedef struct mrb_mt_tbl {
```c
/* ROM table entry: 3rd param is MRB_ARGS_*() optionally OR'd with
MRB_MT_PRIVATE. The macro OR's in MRB_MT_FUNC automatically. */
MRB_MT_PRIVATE. The macro OR's in MRB_MT_FUNC automatically.
`func` must be the first member of `union mrb_mt_ptr` so that
positional initialization works on legacy C++ compilers that do
not accept C99 designated initializers. */
#define MRB_MT_ENTRY(fn, sym, flags) \
{ { .func = (fn) }, (sym), (flags) | MRB_MT_FUNC }
{ { (fn) }, (sym), (flags) | MRB_MT_FUNC }
/* Extract aspec from combined flags */
#define MRB_MT_ASPEC(flags) ((mrb_aspec)((flags) & 0xffffff))
+144 -14
View File
@@ -131,7 +131,9 @@ limit = (GC_STEP_SIZE / 100) * step_ratio
```
With default `step_ratio = 200` and `GC_STEP_SIZE = 1024`, the
limit is 2048 objects per step.
limit is 2048 objects per step. After each step, `gc_debt` is
decremented by the actual number of objects processed, so larger
steps repay more debt.
When the gray stack is exhausted, the final marking phase re-marks
the arena and global variables to catch objects created during
@@ -273,7 +275,7 @@ From Ruby: `GC.generational_mode = true/false`.
`mrb_obj_alloc()` is the core allocation function:
1. If `MRB_GC_STRESS` is defined, run a full GC
2. If `gc->live >= gc->threshold`, run `mrb_incremental_gc()`
2. Increment `gc->gc_debt`; if positive, run `mrb_incremental_gc()`
3. Ensure arena has space (`gc_arena_keep`)
4. Pop an object from the freelist of `gc->free_heaps`
5. If no free pages, allocate a new page (`add_heap`)
@@ -299,18 +301,43 @@ The object's type is set to `MRB_TT_FREE` after freeing.
## Triggering GC
### Automatic
### Debt Model
GC runs automatically when `gc->live >= gc->threshold` during
object allocation. After each cycle:
GC uses a **debt-based feedback model** to balance allocation
rate against collection work. The key field is `gc->gc_debt`
(signed integer):
- **Negative** = credit (GC is ahead, no collection needed)
- **Zero** = balanced
- **Positive** = debt (allocation outpacing collection, GC runs)
Each object allocation increments `gc_debt` by 1. When debt
goes positive, `mrb_incremental_gc()` runs. Each incremental
step decrements debt by `GC_STEP_SIZE` (1024), giving credit
for many future allocations.
When a GC cycle completes, credit is calculated from
`interval_ratio`:
```text
threshold = (live_after_mark / 100) * interval_ratio
credit = (live_after_mark / 100) * interval_ratio - live_after_mark
minimum: GC_STEP_SIZE (1024)
gc_debt = -credit
```
With default `interval_ratio = 200`, GC triggers when live objects
roughly double.
With default `interval_ratio = 200` and 1000 live objects:
`credit = (1000/100)*200 - 1000 = 1000`, so approximately 1000
allocations can occur before the next GC cycle begins.
### Malloc Pressure
When `gc->malloc_threshold` is set (non-zero), the GC also
tracks bytes allocated through `mrb_realloc_simple()` in
`gc->malloc_increase`. When `malloc_increase` exceeds
`malloc_threshold`, the counter resets and an incremental GC
step runs. This captures memory pressure from large buffers
(e.g., long strings) that would otherwise be invisible to the
object-count-based debt model.
### Manual
@@ -333,6 +360,7 @@ From Ruby: `GC.start`.
| `MRB_GC_FIXED_ARENA` | off | Use fixed-size arena |
| `MRB_GC_TURN_OFF_GENERATIONAL` | off | Disable generational mode |
| `MRB_GC_STRESS` | off | Full GC on every allocation (debug) |
| `MRB_GC_STATS` | off | Enable GC statistics counters |
| `MRB_USE_MALLOC_TRIM` | off | Call `malloc_trim()` after full GC |
### Runtime
@@ -340,18 +368,120 @@ From Ruby: `GC.start`.
From Ruby code:
```ruby
GC.interval_ratio = 200 # threshold = live * ratio / 100
GC.step_ratio = 200 # objects per incremental step
GC.interval_ratio = 200 # controls debt credit after GC cycle
GC.step_ratio = 200 # objects per incremental step
GC.step_limit = 0 # 0=unlimited, >0=absolute step cap
GC.malloc_threshold = 0 # 0=disabled, >0=bytes to trigger GC
GC.generational_mode = true
GC.start # force full GC
GC.enable # re-enable GC
GC.disable # disable GC
GC.start # force full GC
GC.enable # re-enable GC
GC.disable # disable GC
```
### GC Statistics
`GC.stat` returns a Hash with GC state and statistics:
```ruby
GC.stat
# => {
# :live => 5432, # live object count
# :debt => -1024, # GC debt (negative=credit, positive=behind)
# :state => 0, # 0=root, 1=marking, 2=sweeping
# :generational => true, # generational mode enabled
# :full => false, # major GC in progress
# :step_limit => 0, # current step limit setting
# :malloc_increase => 8192, # malloc bytes since last cycle
# :malloc_threshold => 0, # current malloc threshold setting
# }
```
With `MRB_GC_STATS` enabled, additional keys are available:
```ruby
# :total => 15, # total GC invocations
# :minor => 12, # minor GC count
# :major => 3, # major GC count
```
### Tuning Guide
**`interval_ratio`** (default 200): Controls how many allocations
occur between GC cycles. Higher values reduce GC frequency but
increase peak memory. The debt credit after each cycle is
`(live_after_mark / 100) * interval_ratio - live_after_mark`.
**`step_ratio`** (default 200): Controls how much work each
incremental step performs. Higher values make each step larger,
reducing total GC overhead but increasing individual pause times.
**`step_limit`** (default 0, unlimited): Caps the maximum work
per incremental step regardless of `step_ratio`. Useful for
real-time applications that need bounded pause times. The
effective step size is `min(step_ratio calculation, step_limit)`.
**`malloc_threshold`** (default 0, disabled): Triggers GC when
cumulative `malloc`/`realloc` bytes exceed this threshold. Useful
when applications allocate large buffers (strings, data objects)
that create memory pressure without proportional object count
increase.
### Practical Tuning Examples
**Allocation-heavy workloads** (many short-lived Procs, closures,
blocks): GC sweep dominates because of high object churn. Increase
`interval_ratio` to reduce GC frequency:
```ruby
GC.interval_ratio = 400 # ~12% faster than default (200)
```
Higher values (400-600) reduce sweep overhead at the cost of more
dead objects accumulating before collection. Values above 600 show
diminishing returns. Peak memory usage increases temporarily, but
live object count after GC remains the same.
**CPU-intensive workloads** (numeric computation, recursive methods
with no object allocation): GC parameters have negligible impact
because GC rarely runs. No tuning needed.
**Real-time or latency-sensitive** applications: Use `step_limit`
to bound pause times:
```ruby
GC.step_limit = 256 # cap incremental step to 256 objects
```
This makes GC pauses more predictable but increases total GC
overhead (more steps needed per cycle).
**Large buffer workloads** (reading files, building long strings):
Set `malloc_threshold` to trigger GC when buffer allocations
accumulate, even if object count is low:
```ruby
GC.malloc_threshold = 1024 * 1024 # trigger GC per ~1MB allocated
```
### Diagnosing GC Overhead
Use `GC.stat` to monitor GC behavior at runtime:
```ruby
s = GC.stat
puts "live objects: #{s[:live]}"
puts "GC debt: #{s[:debt]}" # positive = GC is behind
puts "GC state: #{s[:state]}" # 0=idle, 1=marking, 2=sweeping
```
If `debt` is frequently positive during performance-critical
sections, increase `interval_ratio`. If memory usage is too high,
decrease it.
## Source Files
| File | Contents |
| -------------------- | --------------------------------- |
| `src/gc.c` | GC implementation (~1400 lines) |
| `src/gc.c` | GC implementation |
| `include/mruby/gc.h` | `mrb_gc` structure, public GC API |
| `include/mruby.h` | Arena save/restore macros |
+165 -6
View File
@@ -15,6 +15,23 @@ This document is collecting these limitations.
This document does not contain a complete list of limitations.
Please help to improve it by submitting your findings.
## Features provided by mrbgems
Many Ruby features that CRuby builds into its core are provided by
mrbgems in mruby. Which features are actually available depends on
which mrbgems are linked into the build. The `default.gembox` and
`stdlib.gembox` cover the common cases, but a minimal build can omit
familiar features such as `Kernel#binding` (provided by
`mruby-binding`), `Kernel#catch`/`throw` (by `mruby-catch`),
`Enumerable` extensions, `Comparable`, IO, regular expressions, and
many more.
This is by design rather than a limitation per se. When porting Ruby
code to mruby, a `NoMethodError` or `NameError` often means "the gem
providing this feature is not linked in" rather than "mruby does not
support it." Adding the relevant gem to the build configuration is
usually enough.
## `Kernel.raise` in rescue clause
`Kernel.raise` without arguments does not raise the current exception within
@@ -133,12 +150,6 @@ The re-defined `+` operator does not accept any arguments.
`'ab'`
Behavior of the operator wasn't changed.
## `Kernel#binding` is not supported without mruby-binding gem
`Kernel#binding` method requires the `mruby-binding` gem (included
in the `metaprog` gembox). Without this gem, `binding` is not
available.
## `nil?` redefinition in conditional expressions
Redefinition of `nil?` is ignored in conditional expressions.
@@ -290,3 +301,151 @@ arbitrary-precision integers when included.
(included in the `stdlib` gembox). Even with the gem,
`ObjectSpace.each_object` has limited functionality compared
to CRuby.
## No Implicit Type Conversion (`to_int`, `to_str`, `to_ary`, ...)
mruby does not perform implicit type conversion through methods
like `to_int`, `to_str`, `to_ary`, or `to_hash`. CRuby uses these
to let user-defined classes duck-type as built-in types — for
example `Array#[]` calls `to_int` on its argument, `String#+` calls
`to_str`, and multiple assignment calls `to_ary` on its right-hand
side. mruby's built-in operations require the actual built-in type
and do not consult these conversion methods.
```ruby
class MyInt; def to_int; 42; end; end
class MyStr; def to_str; "x"; end; end
class MyAry; def to_ary; [1,2,3]; end; end
```
#### CRuby
```
[1,2,3][MyInt.new] # => nil (to_int called -> ary[42])
"a" + MyStr.new # => "ax" (to_str called)
a, b, c = MyAry.new # => a=1, b=2, c=3 (to_ary called)
```
#### mruby
```
[1,2,3][MyInt.new] # TypeError
"a" + MyStr.new # TypeError
a, b, c = MyAry.new # a=<MyAry obj>, b=nil, c=nil (treated as single value)
```
Identity versions of `to_int`, `to_str`, `to_sym`, and `to_hash`
remain defined on the corresponding built-in types so that
`respond_to?(:to_str)`-style checks work for built-in instances.
`Float#to_int` and `Array#to_ary` are intentionally not defined.
Explicit conversion methods (`to_i`, `to_s`, `to_a`) work as in
CRuby and are called by features such as string interpolation and
the splat operator (`*obj`).
This is a deliberate trade-off: implicit conversion forces every
coercion site to go through method dispatch and can silently mask
type-mismatch bugs.
## Nested `def` in Singleton-Method Context
`def` written inside a singleton method (`def self.foo`) is placed
on a different class in mruby than in CRuby. CRuby registers the
inner method as an instance method of the lexical enclosing class.
mruby registers it as a method of the enclosing receiver's
singleton class, which makes it visible as a class method of the
enclosing class.
```ruby
class SomeClass
def self.class_method
def nested; 'nested!'; end
end
end
SomeClass.class_method
```
#### CRuby
```
SomeClass.nested # NoMethodError
SomeClass.new.nested # => "nested!" (instance method)
```
#### mruby
```
SomeClass.nested # => "nested!" (class method)
SomeClass.new.nested # NoMethodError
```
Writing nested `def` like this is unusual; this difference rarely
surfaces in practical code.
## `Proc#dup` / `Proc#clone` is Always Orphan
A `dup` or `clone` of a block given to a method is always treated as
an orphan block in mruby — calling it raises `LocalJumpError` if the
block contains `break` or `return`. CRuby is finer-grained: the copy
inherits the orphan status of its original, so the copy only becomes
orphan once the original yielding method returns.
```ruby
def m(&b)
b.dup
end
x = m { break 1 }
x.call
```
#### CRuby
```
LocalJumpError # raised only after m returns; if called inside m,
# the dup is still a live block
```
#### mruby
```
LocalJumpError # always raised — the dup is orphan from the moment
# it is created
```
mruby's stricter rule keeps `RProc` from needing a back-pointer to
the original block (which would also enlarge the GC mark set).
## `Class#initialize` Can Be Re-Invoked
CRuby raises `TypeError: already initialized class` when `initialize`
is invoked on a class that has already been set up. mruby's
`Class#initialize` has no such guard — invoking it on an existing
class through `__send__`, `send`, or `UnboundMethod#bind_call`
silently succeeds. The superclass argument is ignored in this case,
so the call cannot rewrite the class hierarchy; only the block (if
any) is evaluated with the class as receiver.
```ruby
Klass = Class.new
Klass.__send__(:initialize) {}
```
#### CRuby
```
TypeError: already initialized class
```
#### mruby
```
The block is evaluated in the context of Klass; no error is raised.
The superclass is not changed even when one is passed as an argument.
```
`Module#initialize` is re-callable in both implementations, so this
divergence is `Class`-specific. Adding the CRuby check would require
an additional flag bit on every `RClass`; mruby leaves the bit
unspent because no destructive side effects are possible through
this path.
+7
View File
@@ -172,6 +172,13 @@
#define MRB_SYMBOL_LINEAR_THRESHOLD 256
#endif
/* Maximum number of dynamic symbols (created at runtime via to_sym etc.)
Presyms, inline symbols, and mrb_intern_static symbols are excluded.
Set to 0 to disable the limit. */
#ifndef MRB_SYMBOL_MAX
#define MRB_SYMBOL_MAX 4096
#endif
/* obsolete configurations */
#if defined(DISABLE_STDIO) || defined(MRB_DISABLE_STDIO)
# define MRB_NO_STDIO
+54 -9
View File
@@ -113,6 +113,8 @@
#include "mrbconf.h"
typedef struct mrb_state mrb_state;
#include <mruby/common.h>
#include <mruby/value.h>
#include <mruby/gc.h>
@@ -156,8 +158,6 @@ typedef uint32_t mrb_aspec;
typedef struct mrb_irep mrb_irep;
struct mrb_state;
#ifndef MRB_FIXED_STATE_ATEXIT_STACK_SIZE
#define MRB_FIXED_STATE_ATEXIT_STACK_SIZE 5
#endif
@@ -224,7 +224,7 @@ mrb_static_assert_powerof2(MRB_METHOD_CACHE_SIZE);
* @param self The self object
* @return [mrb_value] The function's return value
*/
typedef mrb_value (*mrb_func_t)(struct mrb_state *mrb, mrb_value self);
typedef mrb_value (*mrb_func_t)(mrb_state *mrb, mrb_value self);
typedef struct {
uint32_t flags; /* method flags (no symbol packed) */
@@ -243,9 +243,26 @@ struct mrb_cache_entry {
};
#endif
#ifdef MRB_CONST_CACHE_SIZE
# undef MRB_NO_CONST_CACHE
mrb_static_assert_powerof2(MRB_CONST_CACHE_SIZE);
#else
/* default constant cache size: 64 */
/* cache size needs to be power of 2 */
# define MRB_CONST_CACHE_SIZE (1<<6)
#endif
#ifndef MRB_NO_CONST_CACHE
struct mrb_const_cache_entry {
const struct mrb_irep *irep;
mrb_sym sym;
mrb_value value;
};
#endif
struct mrb_jmpbuf;
typedef void (*mrb_atexit_func)(struct mrb_state*);
typedef void (*mrb_atexit_func)(mrb_state*);
#ifdef MRB_USE_TASK_SCHEDULER
struct mrb_task;
@@ -257,10 +274,12 @@ typedef struct mrb_task_state {
volatile mrb_bool switching; /* Context switch pending flag */
struct mrb_task *main_task; /* Main task wrapper for root context */
uint8_t scheduler_lock; /* Lock counter for synchronous execution */
mrb_bool loop_running; /* Active mrb_task_run loop flag */
mrb_bool exception_as_result; /* Return unhandled task exceptions as values */
} mrb_task_state;
#endif
typedef struct mrb_state {
struct mrb_state {
struct mrb_jmpbuf *jmp;
struct mrb_context *c;
@@ -297,22 +316,28 @@ typedef struct mrb_state {
struct mrb_cache_entry cache[MRB_METHOD_CACHE_SIZE];
#endif
#ifndef MRB_NO_CONST_CACHE
struct mrb_const_cache_entry const_cache[MRB_CONST_CACHE_SIZE];
#endif
mrb_sym symidx;
const char **symtbl;
uint8_t *sym_flags; /* per-symbol flags (SYM_FL_*) */
size_t symcapa;
struct mrb_sym_hash_table *symhash;
void *sym_pool;
mrb_sym dynamic_sym_count; /* count of dynamic (GC-candidate) symbols */
#ifndef MRB_USE_ALL_SYMBOLS
char symbuf[8]; /* buffer for small symbol names */
#endif
#ifdef MRB_USE_DEBUG_HOOK
void (*code_fetch_hook)(struct mrb_state* mrb, const struct mrb_irep *irep, const mrb_code *pc, mrb_value *regs);
void (*debug_op_hook)(struct mrb_state* mrb, const struct mrb_irep *irep, const mrb_code *pc, mrb_value *regs);
void (*code_fetch_hook)(mrb_state* mrb, const struct mrb_irep *irep, const mrb_code *pc, mrb_value *regs);
void (*debug_op_hook)(mrb_state* mrb, const struct mrb_irep *irep, const mrb_code *pc, mrb_value *regs);
#endif
#ifdef MRB_BYTECODE_DECODE_OPTION
mrb_code (*bytecode_decoder)(struct mrb_state* mrb, mrb_code code);
mrb_code (*bytecode_decoder)(mrb_state* mrb, mrb_code code);
#endif
struct RClass *eException_class;
@@ -339,7 +364,7 @@ typedef struct mrb_state {
#ifdef MRB_USE_TASK_SCHEDULER
mrb_task_state task; /* Task scheduler state */
#endif
} mrb_state;
};
/**
* Defines a new class.
@@ -1159,6 +1184,21 @@ MRB_API mrb_value mrb_funcall_id(mrb_state *mrb, mrb_value val, mrb_sym mid, mrb
* @see mrb_funcall
*/
MRB_API mrb_value mrb_funcall_argv(mrb_state *mrb, mrb_value val, mrb_sym name, mrb_int argc, const mrb_value *argv);
/*
* Convenience wrappers for `mrb_funcall_argv` with a fixed argument count.
* Avoids the 16-slot fixed argv buffer used by the variadic `mrb_funcall_id`.
*/
MRB_INLINE mrb_value
mrb_funcall_argv1(mrb_state *mrb, mrb_value val, mrb_sym name, mrb_value a1)
{
return mrb_funcall_argv(mrb, val, name, 1, &a1);
}
MRB_INLINE mrb_value
mrb_funcall_argv2(mrb_state *mrb, mrb_value val, mrb_sym name, mrb_value a1, mrb_value a2)
{
const mrb_value argv[] = { a1, a2 };
return mrb_funcall_argv(mrb, val, name, 2, argv);
}
/**
* Call existing Ruby functions with a block.
*/
@@ -1284,6 +1324,11 @@ MRB_API void mrb_method_cache_clear(mrb_state *mrb);
#else
#define mrb_method_cache_clear(mrb) ((void)0)
#endif
#ifndef MRB_NO_CONST_CACHE
MRB_API void mrb_const_cache_clear(mrb_state *mrb);
#else
#define mrb_const_cache_clear(mrb) ((void)0)
#endif
/**
* Check if mrb_open() failed
+24 -2
View File
@@ -20,7 +20,17 @@ typedef struct mrb_shared_array {
mrb_value *ptr;
} mrb_shared_array;
#if defined(MRB_32BIT) && defined(MRB_NO_BOXING) && (!defined(MRB_USE_FLOAT32) || defined(MRB_INT64)) && !defined(MRB_ARY_NO_EMBED)
/* On 32-bit platforms whose ABI gives 8-byte members 8-byte alignment
(ARM, MIPS, xtensa, ...), an embedded mrb_value array forces 8-byte
alignment of the inner union, padding the heap-form layout and
inflating struct size past the 5-word RVALUE limit. Disable embedding
whenever mrb_value contains an 8-byte aligned member: nan-boxing
(uint64_t), or no-boxing with int64_t/double inside the union. */
#if defined(MRB_32BIT) && \
(defined(MRB_NAN_BOXING) || \
(defined(MRB_NO_BOXING) && \
(!defined(MRB_USE_FLOAT32) || defined(MRB_INT64)))) && \
!defined(MRB_ARY_NO_EMBED)
# define MRB_ARY_NO_EMBED
#endif
@@ -57,7 +67,7 @@ struct RArray {
#define ARY_UNSET_EMBED_FLAG(a) (void)0
#define ARY_EMBED_LEN(a) 0
#define ARY_SET_EMBED_LEN(a,len) (void)0
#define ARY_EMBED_PTR(a) 0
#define ARY_EMBED_PTR(a) ((mrb_value*)NULL)
#else
#define MRB_ARY_EMBED_MASK 7
#define ARY_EMBED_P(a) ((a)->flags & MRB_ARY_EMBED_MASK)
@@ -71,6 +81,7 @@ struct RArray {
#define ARY_PTR(a) (ARY_EMBED_P(a)?ARY_EMBED_PTR(a):(a)->as.heap.ptr)
#define RARRAY_LEN(a) ARY_LEN(RARRAY(a))
#define RARRAY_PTR(a) ARY_PTR(RARRAY(a))
#define RARRAY_GETMEM(a, ptr, len) ARY_GETMEM(RARRAY(a), ptr, len)
#define ARY_SET_LEN(a,n) do {\
if (ARY_EMBED_P(a)) {\
mrb_assert((n) <= MRB_ARY_EMBED_LEN_MAX); \
@@ -84,6 +95,17 @@ struct RArray {
#define ARY_SHARED_P(a) ((a)->flags & MRB_ARY_SHARED)
#define ARY_SET_SHARED_FLAG(a) ((a)->flags |= MRB_ARY_SHARED)
#define ARY_UNSET_SHARED_FLAG(a) ((a)->flags &= ~MRB_ARY_SHARED)
#define ARY_GETMEM(a, ptr, len) do { \
struct RArray *MRB_UNIQNAME(_a_) = (a); \
if (ARY_EMBED_P(MRB_UNIQNAME(_a_))) { \
(len) = ARY_EMBED_LEN(MRB_UNIQNAME(_a_)); \
(ptr) = ARY_EMBED_PTR(MRB_UNIQNAME(_a_)); \
} \
else { \
(len) = MRB_UNIQNAME(_a_)->as.heap.len; \
(ptr) = MRB_UNIQNAME(_a_)->as.heap.ptr; \
} \
} while (0)
MRB_API void mrb_ary_modify(mrb_state*, struct RArray*);
MRB_API mrb_value mrb_ary_dup(mrb_state*, mrb_value ary);
+3 -2
View File
@@ -103,7 +103,8 @@ mrb_unboxed_type(mrb_value o)
{
if (!mrb_float_p(o) && mrb_nb_tt(o) == MRB_NANBOX_TT_OBJECT && o.u != 0) {
return ((struct RBasic*)(uintptr_t)o.u)->tt;
} else {
}
else {
return MRB_TT_FALSE;
}
}
@@ -150,7 +151,7 @@ mrb_nan_boxing_value_int(mrb_value v)
#define SET_TRUE_VALUE(r) NANBOX_SET_MISC_VALUE(r, MRB_TT_TRUE, 1)
#define SET_BOOL_VALUE(r,b) NANBOX_SET_MISC_VALUE(r, (b) ? MRB_TT_TRUE : MRB_TT_FALSE, 1)
#ifdef MRB_INT64
MRB_API mrb_value mrb_boxing_int_value(struct mrb_state*, mrb_int);
MRB_API mrb_value mrb_boxing_int_value(mrb_state*, mrb_int);
#define SET_INT_VALUE(mrb, r, n) ((r) = mrb_boxing_int_value(mrb, n))
#else
#define SET_INT_VALUE(mrb, r, n) SET_FIXNUM_VALUE(r, n)
+7 -5
View File
@@ -165,11 +165,11 @@ mrb_val_union(mrb_value v)
return x;
}
MRB_API mrb_value mrb_word_boxing_cptr_value(struct mrb_state*, void*);
MRB_API mrb_value mrb_word_boxing_cptr_value(mrb_state*, void*);
#ifndef MRB_NO_FLOAT
MRB_API mrb_value mrb_word_boxing_float_value(struct mrb_state*, mrb_float);
MRB_API mrb_value mrb_word_boxing_float_value(mrb_state*, mrb_float);
#endif
MRB_API mrb_value mrb_boxing_int_value(struct mrb_state*, mrb_int);
MRB_API mrb_value mrb_boxing_int_value(mrb_state*, mrb_int);
#if WORDBOX_IMMEDIATE_MASK == 0x3
#define mrb_immediate_p(o) ((o).w & WORDBOX_IMMEDIATE_MASK || (o).w <= MRB_Qundef)
@@ -266,9 +266,11 @@ mrb_unboxed_type(mrb_value o)
{
if (mrb_nil_p(o)) {
return MRB_TT_FALSE;
} else if ((o.w & WORDBOX_IMMEDIATE_MASK) == 0) {
}
else if ((o.w & WORDBOX_IMMEDIATE_MASK) == 0) {
return mrb_val_union(o).bp->tt;
} else {
}
else {
return MRB_TT_FALSE;
}
}
+6 -3
View File
@@ -100,10 +100,13 @@ void mrb_mc_clear_by_class(mrb_state *mrb, struct RClass* c);
typedef int (mrb_mt_foreach_func)(mrb_state*,mrb_sym,mrb_method_t,void*);
MRB_API void mrb_mt_foreach(mrb_state*, struct RClass*, mrb_mt_foreach_func*, void*);
/* ROM method table types for static method registration */
/* ROM method table types for static method registration.
* NOTE: `func` is kept as the first union member so that positional
* aggregate initialization in MRB_MT_ENTRY works without C99
* designated initializers (required for legacy C++ compilers). */
union mrb_mt_ptr {
const struct RProc *proc;
mrb_func_t func;
const struct RProc *proc;
};
/* entry combining function pointer, symbol key, and flags */
@@ -128,7 +131,7 @@ typedef struct mrb_mt_tbl {
/* ROM table entry: 3rd param is MRB_ARGS_*() optionally OR'd with MRB_MT_PRIVATE. */
#define MRB_MT_ENTRY(fn, sym, flags) \
{ { .func = (fn) }, (sym), (flags) | MRB_MT_FUNC }
{ { (fn) }, (sym), (flags) | MRB_MT_FUNC }
#define MRB_MT_ASPEC(flags) ((mrb_aspec)((flags) & 0xffffff))
/* "removed" tombstone: MRB_MT_FUNC flag set with NULL function pointer.
+1
View File
@@ -134,6 +134,7 @@ struct mrb_parser_state {
mrb_sym* filename_table;
uint16_t filename_table_length;
uint16_t current_filename_index;
uint16_t prev_file_lineno; /* saved lineno before partial_hook file switch */
/* Variable-sized node management */
mrb_ast_node *nvars;
+27 -6
View File
@@ -37,7 +37,14 @@ MRB_API mrb_value mrb_exc_new_str(mrb_state *mrb, struct RClass* c, mrb_value st
#define mrb_exc_new_lit(mrb, c, lit) mrb_exc_new_str(mrb, c, mrb_str_new_lit(mrb, lit))
MRB_API mrb_noreturn void mrb_no_method_error(mrb_state *mrb, mrb_sym id, mrb_value args, const char *fmt, ...);
#if defined(MRB_NAN_BOXING) || defined(MRB_WORD_BOXING) || defined(MRB_64BIT)
/* On 32-bit platforms whose ABI gives uint64_t/double 8-byte alignment
(ARM, MIPS, PowerPC, xtensa, ...), embedding mrb_value directly in
RBreak forces 8-byte alignment that pushes the struct past the 5-word
RVALUE budget via padding. Store the value bits as a uint32_t array
(4-byte aligned) to dodge that padding. Word-boxing's mrb_value is
just a uintptr_t with no over-alignment, and 64-bit platforms have no
alignment gap to begin with, so neither needs the workaround. */
#if defined(MRB_64BIT) || defined(MRB_WORD_BOXING)
#undef MRB_USE_RBREAK_VALUE_UNION
#else
#define MRB_USE_RBREAK_VALUE_UNION 1
@@ -45,7 +52,8 @@ MRB_API mrb_noreturn void mrb_no_method_error(mrb_state *mrb, mrb_sym id, mrb_va
/*
* flags:
* 0..7: enum mrb_vtype (only when defined MRB_USE_RBREAK_VALUE_UNION)
* 0..7: enum mrb_vtype (only when MRB_USE_RBREAK_VALUE_UNION and
* !MRB_NAN_BOXING; nan-boxing encodes the type in the bits)
* 8..10: RBREAK_TAGs in src/vm.c (otherwise, set to 0)
*/
struct RBreak {
@@ -53,11 +61,11 @@ struct RBreak {
uintptr_t ci_break_index; // The top-level ci index to break. One before the return destination.
#ifndef MRB_USE_RBREAK_VALUE_UNION
mrb_value val;
#elif defined(MRB_NAN_BOXING)
/* nan-boxing: mrb_value is a single 64-bit word (type encoded in NaN bits) */
uint32_t value[sizeof(mrb_value) / sizeof(uint32_t)];
#else
/* Store value as uint32_t words instead of union mrb_value_union
to avoid 8-byte alignment of int64_t/double on 32-bit platforms
(e.g., ARM, MIPS, PowerPC) which would inflate struct size beyond
the 5-word RVALUE limit due to padding. */
/* no-boxing: store only the union bits; tt goes in flags */
uint32_t value[sizeof(union mrb_value_union) / sizeof(uint32_t)];
#endif
};
@@ -65,6 +73,19 @@ struct RBreak {
#ifndef MRB_USE_RBREAK_VALUE_UNION
#define mrb_break_value_get(brk) ((brk)->val)
#define mrb_break_value_set(brk, v) ((brk)->val = v)
#elif defined(MRB_NAN_BOXING)
static inline mrb_value
mrb_break_value_get(struct RBreak *brk)
{
mrb_value val;
memcpy(&val, brk->value, sizeof(val));
return val;
}
static inline void
mrb_break_value_set(struct RBreak *brk, mrb_value val)
{
memcpy(brk->value, &val, sizeof(val));
}
#else
#define RBREAK_VALUE_TT_MASK ((1 << 8) - 1)
static inline mrb_value
+15 -9
View File
@@ -14,15 +14,12 @@
*/
MRB_BEGIN_DECL
struct mrb_state;
#define MRB_EACH_OBJ_OK 0
#define MRB_EACH_OBJ_BREAK 1
typedef int (mrb_each_object_callback)(struct mrb_state *mrb, struct RBasic *obj, void *data);
void mrb_objspace_each_objects(struct mrb_state *mrb, mrb_each_object_callback *callback, void *data);
typedef int (mrb_each_object_callback)(mrb_state *mrb, struct RBasic *obj, void *data);
void mrb_objspace_each_objects(mrb_state *mrb, mrb_each_object_callback *callback, void *data);
size_t mrb_objspace_page_slot_size(void);
MRB_API void mrb_free_context(struct mrb_state *mrb, struct mrb_context *c);
MRB_API void mrb_free_context(mrb_state *mrb, struct mrb_context *c);
#ifndef MRB_GC_ARENA_SIZE
#define MRB_GC_ARENA_SIZE 100
@@ -48,7 +45,7 @@ typedef struct mrb_gc {
mrb_bool gray_overflow:1; /* gray stack overflowed; needs heap rescan */
size_t live; /* count of live objects */
size_t live_after_mark; /* old generation objects */
size_t threshold; /* threshold to start GC */
mrb_int gc_debt; /* <0:credit, >0:needs GC */
size_t oldgen_threshold; /* threshold to kick major GC */
mrb_gc_state state; /* current state of gc */
int interval_ratio;
@@ -59,6 +56,9 @@ typedef struct mrb_gc {
mrb_bool generational :1; /* generational GC mode */
mrb_bool full :1; /* major GC mode */
mrb_bool out_of_memory :1; /* out-of-memory error occurred */
size_t step_limit; /* 0=unlimited, >0=absolute step cap */
size_t malloc_increase; /* malloc bytes since last GC cycle */
size_t malloc_threshold; /* 0=disabled, >0=bytes to trigger GC */
#ifdef MRB_GC_FIXED_ARENA
struct RBasic *arena[MRB_GC_ARENA_SIZE]; /* GC protection array */
@@ -67,10 +67,16 @@ typedef struct mrb_gc {
int arena_capa; /* size of protection array */
#endif
int arena_idx;
#ifdef MRB_GC_STATS
uint32_t gc_total_count; /* total GC invocations */
uint32_t minor_gc_count; /* minor GC count */
uint32_t major_gc_count; /* major GC count */
#endif
} mrb_gc;
MRB_API mrb_bool mrb_object_dead_p(struct mrb_state *mrb, struct RBasic *object);
MRB_API int mrb_gc_add_region(struct mrb_state *mrb, void *start, size_t size);
MRB_API mrb_bool mrb_object_dead_p(mrb_state *mrb, struct RBasic *object);
MRB_API int mrb_gc_add_region(mrb_state *mrb, void *start, size_t size);
#define MRB_GC_RED 7
+3
View File
@@ -279,4 +279,7 @@ mrb_value mrb_bint_abs(mrb_state *mrb, mrb_value x);
void mrb_task_mark_all(mrb_state *mrb);
#endif
/* Internal object allocation without type validation (gc.c) */
struct RBasic* mrb_obj_alloc_core(mrb_state*, enum mrb_vtype, struct RClass*);
#endif /* MRUBY_INTERNAL_H */
+1 -1
View File
@@ -20,7 +20,7 @@ enum irep_pool_type {
IREP_TT_SSTR = 2, /* string (static) */
IREP_TT_INT32 = 1, /* 32-bit integer */
IREP_TT_INT64 = 3, /* 64-bit integer */
IREP_TT_BIGINT = 7, /* big integer (not yet supported) */
IREP_TT_BIGINT = 7, /* big integer */
IREP_TT_FLOAT = 5, /* float (double/float) */
};
+54
View File
@@ -102,6 +102,60 @@ struct RProc {
#define MRB_PROC_ALIAS 8192
#define MRB_PROC_ALIAS_P(p) (((p)->flags & MRB_PROC_ALIAS) != 0)
/* Compressed aspec for cfunc procs (13 bits in RProc.flags).
* Uses free bits 0-6 and 14-19 to store a compressed argument spec.
* Layout: block(0) kdict(1) key(2-3) post(4-5) rest(6) opt(14-16) req(17-19)
* Field widths are smaller than the full 24-bit aspec: req/opt max 7, post/key max 3.
* Values exceeding the compressed range are clamped and rest is forced to 1. */
#define MRB_PROC_CASPEC_MASK 0xfc07fu /* bits 0-6 and 14-19 */
static inline uint32_t
mrb_proc_compress_aspec(mrb_aspec aspec)
{
uint32_t req = MRB_ASPEC_REQ(aspec);
uint32_t opt = MRB_ASPEC_OPT(aspec);
uint32_t rest = MRB_ASPEC_REST(aspec);
uint32_t post = MRB_ASPEC_POST(aspec);
uint32_t key = MRB_ASPEC_KEY(aspec);
uint32_t kdict = MRB_ASPEC_KDICT(aspec);
uint32_t block = MRB_ASPEC_BLOCK(aspec);
if (req > 7 || opt > 7 || post > 3 || key > 3) {
if (req > 7) req = 7;
if (opt > 7) opt = 7;
if (post > 3) post = 3;
if (key > 3) key = 3;
rest = 1;
}
return block | (kdict << 1) | (key << 2) | (post << 4) | (rest << 6)
| (opt << 14) | (req << 17);
}
static inline mrb_aspec
mrb_proc_decompress_caspec(uint32_t flags)
{
return (((flags >> 17) & 0x7) << 18) /* req */
| (((flags >> 14) & 0x7) << 13) /* opt */
| (((flags >> 6) & 0x1) << 12) /* rest */
| (((flags >> 4) & 0x3) << 7) /* post */
| (((flags >> 2) & 0x3) << 2) /* key */
| (((flags >> 1) & 0x1) << 1) /* kdict */
| (flags & 0x1); /* block */
}
static inline void
mrb_proc_set_cfunc_aspec(struct RProc *p, mrb_aspec aspec)
{
p->flags &= ~(MRB_PROC_NOARG | MRB_PROC_CASPEC_MASK);
if (aspec == 0) {
p->flags |= MRB_PROC_NOARG;
}
else {
p->flags |= mrb_proc_compress_aspec(aspec);
}
}
#define mrb_proc_ptr(v) ((struct RProc*)(mrb_ptr(v)))
struct RProc *mrb_proc_new(mrb_state*, const mrb_irep*);
+10
View File
@@ -342,6 +342,16 @@ MRB_API const char *mrb_string_value_cstr(mrb_state *mrb, mrb_value *str);
*/
MRB_API mrb_value mrb_str_dup(mrb_state *mrb, mrb_value str);
/**
* Returns a frozen string object.
* The string will be duplicated and frozen if it is not already frozen.
*
* @param mrb The current mruby state.
* @param str An original Ruby string.
* @return [mrb_value] Ruby frozen string.
*/
MRB_API mrb_value mrb_str_dup_frozen(mrb_state *mrb, mrb_value str);
/**
* Returns a symbol from a passed in Ruby string.
*
+3 -5
View File
@@ -55,8 +55,6 @@ typedef uint8_t mrb_bool;
# endif
#endif
struct mrb_state;
#if defined _MSC_VER && _MSC_VER < 1800
# define PRIo64 "llo"
# define PRId64 "lld"
@@ -330,7 +328,7 @@ struct RCptr {
*/
#ifndef MRB_NO_FLOAT
MRB_INLINE mrb_value
mrb_float_value(struct mrb_state *mrb, mrb_float f)
mrb_float_value(mrb_state *mrb, mrb_float f)
{
mrb_value v;
(void) mrb;
@@ -340,7 +338,7 @@ mrb_float_value(struct mrb_state *mrb, mrb_float f)
#endif
MRB_INLINE mrb_value
mrb_cptr_value(struct mrb_state *mrb, void *p)
mrb_cptr_value(mrb_state *mrb, void *p)
{
mrb_value v;
(void) mrb;
@@ -352,7 +350,7 @@ mrb_cptr_value(struct mrb_state *mrb, void *p)
* Returns an integer in Ruby.
*/
MRB_INLINE mrb_value
mrb_int_value(struct mrb_state *mrb, mrb_int i)
mrb_int_value(mrb_state *mrb, mrb_int i)
{
mrb_value v;
SET_INT_VALUE(mrb, v, i);
+1 -2
View File
@@ -41,7 +41,7 @@ module MRuby
allocf.c
readnum.c
readint.c
readfloat.c
fp_uscale.c
state.c
symbol.c
class.c
@@ -66,7 +66,6 @@ module MRuby
cdump.c
codedump.c
print.c
fmt_fp.c
debug.c
etc.c
version.c
+26 -1
View File
@@ -81,7 +81,7 @@ module MRuby
include LoadGems
attr_accessor :name, :bins, :exts, :file_separator, :build_dir, :gem_clone_dir, :defines, :libdir_name
attr_reader :products, :libmruby_core_objs, :libmruby_objs, :gems, :toolchains, :presym, :mrbc_build, :gem_dir_to_repo_url
attr_reader :install_excludes
attr_reader :install_excludes, :port_names
alias libmruby libmruby_objs
@@ -138,6 +138,7 @@ module MRuby
@mrbcfile_external = false
@internal = internal
@toolchains = []
@port_names = nil
@gem_dir_to_repo_url = {}
# Add lambda instead of string because libdir_name or lib may be changed by user configuration
@@ -183,6 +184,30 @@ module MRuby
@enable_debug = true
end
# Set target port names for this build.
# Each gem compiles the first matching ports/<name>/ directory;
# later names in the list act as fallbacks for gems that don't
# ship a port for the earlier names.
# conf.ports :esp32
# conf.ports :rp2040, :posix # use rp2040 if available, else posix
def ports(*names)
@port_names = names.map { |n| n.to_s }
end
# Returns the effective port names for this build.
# If not explicitly set, auto-detects :posix or :win for host builds.
def effective_ports
return @port_names if @port_names
if kind_of?(MRuby::CrossBuild)
[]
elsif ENV['OS'] == 'Windows_NT' ||
('A'..'Z').any? { |v| Dir.exist?("#{v}:") }
['win']
else
['posix']
end
end
def disable_lock
@enable_lock = false
end
+10 -5
View File
@@ -343,16 +343,21 @@ module MRuby
opt = @compile_options % {funcname: funcname}
opt << " -S" if cdump
opt << " -s" if static
# Have mrbc write to a private tempfile (-o) instead of stdout (-o-)
# to avoid pipe-inheritance races with parallel rake on Windows MinGW,
# where unrelated _pp build-progress lines from sibling workers can
# leak into the captured stdout and corrupt the generated C file.
tmpout = "#{out.path}.#{funcname}.mrbcout"
opt = opt.sub(/\s-o-(?=\s|\z)/, %Q[ -o "#{filename tmpout}"])
cmd = %["#{filename @command}" #{opt} #{filename(infiles).map{|f| %["#{f}"]}.join(' ')}]
puts cmd if Rake.verbose
IO.popen(cmd, 'r') do |io|
out.puts io.read
end
# if mrbc execution fail, drop the file
unless $?.success?
unless system(cmd)
rm_f tmpout
rm_f out.path
fail "Command failed with status (#{$?.exitstatus}): [#{cmd[0,42]}...]"
end
out.write File.binread(tmpout)
rm_f tmpout
end
end
+54
View File
@@ -25,6 +25,7 @@ module MRuby
alias :author= :authors=
attr_accessor :rbfiles, :objs
attr_reader :port_objs
attr_writer :test_objs, :test_rbfiles
attr_accessor :test_args, :test_preload
@@ -43,6 +44,7 @@ module MRuby
def initialize(name, &block)
@name = name
@initializer = block
@post_user_config = nil
@version = "0.0.0"
@dependencies = []
@conflicts = []
@@ -59,6 +61,22 @@ module MRuby
@rbfiles = Dir.glob("#{@dir}/mrblib/**/*.rb").sort
@objs = srcs_to_objs("src")
# Add platform-specific sources from the first matching
# ports/<name>/ directory. effective_ports is a fallback
# chain: later names act as defaults for gems that don't ship
# a port for the earlier names. These objs are tracked
# separately so List#resolve_external_hal! can drop them when
# an external HAL provider (gem named hal-<short>-*) is loaded.
@port_objs = []
build.effective_ports.each do |port|
port_dir = "#{@dir}/ports/#{port}"
if File.directory?(port_dir)
@port_objs = srcs_to_objs("ports/#{port}")
@objs += @port_objs
break
end
end
@test_preload = nil # 'test/assert.rb'
@test_args = {}
@skip_test = false
@@ -85,6 +103,7 @@ module MRuby
build.libmruby_objs << @objs
instance_eval(&@build_config_initializer) if @build_config_initializer
instance_eval(&@post_user_config) if @post_user_config
repo_url = build.gem_dir_to_repo_url[dir]
build.locks[repo_url]['version'] = version if repo_url
@@ -194,6 +213,19 @@ module MRuby
end
end
# Register a block that runs after the user's `build.gem` block has
# been processed. Intended for gem authors to fill in defaults that
# depend on user-supplied configuration (e.g. auto-detect a library
# only if the user didn't specify which to use).
#
# Initialization order:
# 1. block in `MRuby::Gem::Specification.new` (gem author)
# 2. block in `build.gem` (user's build_config)
# 3. block in `post_user_config` (gem author, this hook)
def post_user_config(&block)
@post_user_config = block
end
def build_settings(&blk)
@build_settings = blk
end
@@ -430,6 +462,28 @@ module MRuby
self.each(&:setup)
gemset = self.setup_dependencies(build).keys.sort
end until gemset == gemset_prev
resolve_external_hal!
end
# A gem named `hal-<short>-<conf>` is treated as the external
# HAL provider for the gem whose name's last `-`-separated
# segment is <short> (e.g., hal-task-glib provides the HAL for
# mruby-task). The target gem's ports/* sources are dropped
# from its object list -- the matching gem supplies the
# implementation. Two or more matches is a build error.
def resolve_external_hal!
each do |target|
next if target.port_objs.nil? || target.port_objs.empty?
short = target.name.split('-').last
pattern = /\Ahal-#{Regexp.escape(short)}-.+\z/
overriders = select { |g| g != target && g.name =~ pattern }
next if overriders.empty?
if overriders.size > 1
fail "Multiple HAL providers for '#{target.name}': " +
overriders.map(&:name).join(", ")
end
target.objs.reject! { |o| target.port_objs.include?(o) }
end
end
def setup_build
-7
View File
@@ -1,7 +0,0 @@
MRuby::Gem::Specification.new('hal-posix-dir') do |spec|
spec.license = 'MIT'
spec.authors = 'mruby developers'
spec.summary = 'POSIX HAL for mruby-dir (Linux, macOS, BSD, Unix)'
spec.add_dependency 'mruby-dir', core: 'mruby-dir'
end
-8
View File
@@ -1,8 +0,0 @@
MRuby::Gem::Specification.new('hal-posix-io') do |spec|
spec.license = 'MIT'
spec.author = 'mruby developers'
spec.summary = 'POSIX HAL for mruby-io (Linux, macOS, BSD, Unix)'
# HAL gem depends on feature gem - brings in mruby-io automatically
spec.add_dependency 'mruby-io', core: 'mruby-io'
end
-8
View File
@@ -1,8 +0,0 @@
MRuby::Gem::Specification.new('hal-posix-socket') do |spec|
spec.license = 'MIT'
spec.author = 'mruby developers'
spec.summary = 'POSIX HAL for mruby-socket (Linux, macOS, BSD, Unix)'
# HAL gem depends on feature gem - brings in mruby-socket automatically
spec.add_dependency 'mruby-socket', core: 'mruby-socket'
end
-102
View File
@@ -1,102 +0,0 @@
# hal-posix-task
POSIX Hardware Abstraction Layer (HAL) implementation for mruby-task.
## Description
Provides timer and interrupt support for the mruby-task cooperative scheduler on POSIX-compliant platforms. Uses `SIGALRM` and `setitimer()` for periodic timer ticks, and `sigprocmask()` for interrupt protection.
## Supported Platforms
- Linux
- macOS
- BSD (FreeBSD, OpenBSD, NetBSD)
- Other POSIX-compliant Unix systems
## Requirements
- POSIX-compliant operating system
- Signal support (`SIGALRM`, `sigaction`, `sigprocmask`)
- Timer support (`setitimer`, `ITIMER_REAL`)
## Usage
### Explicit HAL Selection (Recommended)
```ruby
MRuby::Build.new do |conf|
# ... other configuration ...
# Specify POSIX HAL - automatically brings in mruby-task
conf.gem core: 'hal-posix-task'
end
```
### Auto-detection (Development)
```ruby
MRuby::Build.new do |conf|
# ... other configuration ...
# Auto-detects and selects hal-posix-task on POSIX platforms
conf.gem core: 'mruby-task'
end
```
## Implementation Details
### Timer Mechanism
- Uses `setitimer(ITIMER_REAL, ...)` to generate periodic `SIGALRM` signals
- Timer interval configured by `MRB_TICK_UNIT` (default: 4ms)
- Signal handler calls `mrb_tick()` for all registered VM instances
### Interrupt Protection
- Critical sections protected using `sigprocmask()` to block `SIGALRM`
- Prevents race conditions during task queue modifications
- Supports nested critical sections through signal masking
### Multi-VM Support
- Supports up to `MRB_TASK_MAX_VMS` concurrent mruby VM instances (default: 8)
- Single shared timer ticks all registered VMs
- Per-VM task counters optimize timer usage (timer disabled when idle)
### Timer Optimization
The implementation dynamically enables/disables the timer based on task state:
- **Timer enabled** when: Multiple ready tasks OR any waiting tasks exist
- **Timer disabled** when: Single task or all tasks dormant/suspended
- Reduces CPU usage and power consumption when scheduler is idle
## Configuration
Override these macros in your build config if needed:
```ruby
conf.gem core: 'hal-posix-task' do |spec|
# Custom tick interval (10ms instead of default 4ms)
spec.build.defines << 'MRB_TICK_UNIT=10'
# Custom timeslice (5 ticks instead of default 3)
spec.build.defines << 'MRB_TIMESLICE_TICK_COUNT=5'
# More concurrent VMs (16 instead of default 8)
spec.build.defines << 'MRB_TASK_MAX_VMS=16'
end
```
## Known Limitations
- `SIGALRM` conflicts with other code using the same signal
- Timer resolution limited by platform (typically 1-10ms)
- Signal delivery may be delayed under heavy system load
- Not suitable for hard real-time requirements
## See Also
- `mruby-task` - Core task scheduler
- `hal-win-task` - Windows HAL implementation
- Task scheduler documentation: `mrbgems/mruby-task/README.md`
-8
View File
@@ -1,8 +0,0 @@
MRuby::Gem::Specification.new('hal-posix-task') do |spec|
spec.license = 'MIT'
spec.authors = 'mruby developers'
spec.summary = 'POSIX HAL for mruby-task (Linux, macOS, BSD, Unix)'
# HAL gem depends on feature gem - brings in mruby-task automatically
spec.add_dependency 'mruby-task', core: 'mruby-task'
end
-7
View File
@@ -1,7 +0,0 @@
MRuby::Gem::Specification.new('hal-win-dir') do |spec|
spec.license = 'MIT and MIT-like license'
spec.authors = ['mruby developers', 'Kevlin Henney']
spec.summary = 'Windows HAL for mruby-dir'
spec.add_dependency 'mruby-dir', core: 'mruby-dir'
end
-11
View File
@@ -1,11 +0,0 @@
MRuby::Gem::Specification.new('hal-win-io') do |spec|
spec.license = 'MIT'
spec.author = 'mruby developers'
spec.summary = 'Windows HAL for mruby-io (Windows, MinGW)'
# HAL gem depends on feature gem - brings in mruby-io automatically
spec.add_dependency 'mruby-io', core: 'mruby-io'
# Link Windows socket library
spec.linker.libraries << "ws2_32"
end
-12
View File
@@ -1,12 +0,0 @@
MRuby::Gem::Specification.new('hal-win-socket') do |spec|
spec.license = 'MIT'
spec.author = 'mruby developers'
spec.summary = 'Windows HAL for mruby-socket (Windows, MinGW)'
# HAL gem depends on feature gem - brings in mruby-socket automatically
spec.add_dependency 'mruby-socket', core: 'mruby-socket'
# Link Windows socket libraries
spec.linker.libraries << "wsock32"
spec.linker.libraries << "ws2_32"
end
-176
View File
@@ -1,176 +0,0 @@
/*
** socket_hal.c - Windows HAL implementation for mruby-socket
**
** See Copyright Notice in mruby.h
**
** Windows implementation for socket operations using Winsock APIs.
** Supported platforms: Windows, MinGW
*/
#ifndef _WIN32_WINNT
#define _WIN32_WINNT 0x0501 // need Windows XP or later
#endif
#include <mruby.h>
#include <mruby/string.h>
#include <mruby/class.h>
#include <mruby/error.h>
#include "socket_hal.h"
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
#include <errno.h>
#include <string.h>
/*
* Socket HAL Initialization/Finalization
*/
void
mrb_hal_socket_init(mrb_state *mrb)
{
WSADATA wsaData;
int result = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (result != NO_ERROR) {
mrb_raise(mrb, mrb_class_get_id(mrb, MRB_SYM(RuntimeError)), "WSAStartup failed");
}
}
void
mrb_hal_socket_final(mrb_state *mrb)
{
(void)mrb;
WSACleanup();
}
/*
* Socket Control Operations
*/
int
mrb_hal_socket_set_nonblock(mrb_state *mrb, int fd, int nonblock)
{
(void)mrb;
u_long mode = nonblock ? 1 : 0;
int result = ioctlsocket(fd, FIONBIO, &mode);
if (result != NO_ERROR) {
return -1;
}
return 0;
}
/*
* Address Conversion Functions
*/
const char*
mrb_hal_socket_inet_ntop(int af, const void *src, char *dst, size_t size)
{
if (af == AF_INET) {
struct sockaddr_in in = {0};
in.sin_family = AF_INET;
memcpy(&in.sin_addr, src, sizeof(struct in_addr));
if (getnameinfo((struct sockaddr*)&in, sizeof(struct sockaddr_in),
dst, (DWORD)size, NULL, 0, NI_NUMERICHOST) == 0) {
return dst;
}
return NULL;
}
else if (af == AF_INET6) {
struct sockaddr_in6 in = {0};
in.sin6_family = AF_INET6;
memcpy(&in.sin6_addr, src, sizeof(struct in6_addr));
if (getnameinfo((struct sockaddr*)&in, sizeof(struct sockaddr_in6),
dst, (DWORD)size, NULL, 0, NI_NUMERICHOST) == 0) {
return dst;
}
return NULL;
}
return NULL;
}
int
mrb_hal_socket_inet_pton(int af, const char *src, void *dst)
{
struct addrinfo hints = {0};
hints.ai_family = af;
hints.ai_flags = AI_NUMERICHOST;
struct addrinfo *res;
if (getaddrinfo(src, NULL, &hints, &res) != 0) {
return 0; /* Invalid address */
}
if (res == NULL) {
return 0;
}
if (af == AF_INET && res->ai_family == AF_INET) {
memcpy(dst, &((struct sockaddr_in*)res->ai_addr)->sin_addr, sizeof(struct in_addr));
freeaddrinfo(res);
return 1;
}
else if (af == AF_INET6 && res->ai_family == AF_INET6) {
memcpy(dst, &((struct sockaddr_in6*)res->ai_addr)->sin6_addr, sizeof(struct in6_addr));
freeaddrinfo(res);
return 1;
}
freeaddrinfo(res);
return 0;
}
/*
* Platform-Specific Socket Features
*/
mrb_value
mrb_hal_socket_sockaddr_un(mrb_state *mrb, const char *path, size_t pathlen)
{
(void)path;
(void)pathlen;
mrb_raise(mrb, mrb_class_get_id(mrb, MRB_SYM(NotImplementedError)),
"sockaddr_un unsupported on Windows");
return mrb_nil_value();
}
int
mrb_hal_socket_socketpair(mrb_state *mrb, int domain, int type, int protocol, int sv[2])
{
(void)mrb;
(void)domain;
(void)type;
(void)protocol;
(void)sv;
/* socketpair is not supported on Windows */
errno = ENOSYS;
return -1;
}
mrb_value
mrb_hal_socket_unix_path(mrb_state *mrb, const char *sockaddr, size_t socklen)
{
(void)sockaddr;
(void)socklen;
mrb_raise(mrb, mrb_class_get_id(mrb, MRB_SYM(NotImplementedError)),
"unix_path unsupported on Windows");
return mrb_nil_value();
}
/*
* Gem initialization
*/
void
mrb_hal_win_socket_gem_init(mrb_state *mrb)
{
(void)mrb;
/* HAL interface functions are called by mruby-socket gem */
}
void
mrb_hal_win_socket_gem_final(mrb_state *mrb)
{
(void)mrb;
/* Cleanup handled by mrb_hal_socket_final called from mruby-socket */
}
-109
View File
@@ -1,109 +0,0 @@
# hal-win-task
Windows Hardware Abstraction Layer (HAL) implementation for mruby-task.
## Description
Provides timer and interrupt support for the mruby-task cooperative scheduler on Windows platforms. Uses multimedia timer (`timeSetEvent`/`timeKillEvent`) for periodic timer ticks, and `CRITICAL_SECTION` for interrupt protection.
## Supported Platforms
- Windows 7 and later
- Windows Server 2008 R2 and later
- All versions with multimedia timer support
## Requirements
- Windows operating system
- Multimedia timer API (`winmm.lib`)
- Visual C++, MinGW, or compatible compiler
## Usage
### Explicit HAL Selection (Recommended)
```ruby
MRuby::Build.new do |conf|
# ... other configuration ...
# Specify Windows HAL - automatically brings in mruby-task
conf.gem core: 'hal-win-task'
end
```
### Auto-detection (Development)
```ruby
MRuby::Build.new do |conf|
# ... other configuration ...
# Auto-detects and selects hal-win-task on Windows platforms
conf.gem core: 'mruby-task'
end
```
## Implementation Details
### Timer Mechanism
- Uses Windows multimedia timer (`timeSetEvent`) for periodic callbacks
- Timer interval configured by `MRB_TICK_UNIT` (default: 4ms)
- `TIME_KILL_SYNCHRONOUS` flag ensures clean timer shutdown
- Requests 1ms timer resolution via `timeBeginPeriod(1)`
### Interrupt Protection
- Critical sections protected using `CRITICAL_SECTION` objects
- Prevents race conditions during task queue modifications
- `EnterCriticalSection`/`LeaveCriticalSection` used for mutual exclusion
- Supports nested critical sections (automatic lock counting)
### Multi-VM Support
- Supports up to `MRB_TASK_MAX_VMS` concurrent mruby VM instances (default: 8)
- Single shared timer ticks all registered VMs
- Per-VM task counters optimize timer usage (timer disabled when idle)
- Interlocked operations (`InterlockedIncrement`/`InterlockedDecrement`) for thread safety
### Timer Optimization
The implementation dynamically enables/disables the timer based on task state:
- **Timer enabled** when: Multiple ready tasks OR any waiting tasks exist
- **Timer disabled** when: Single task or all tasks dormant/suspended
- Reduces CPU usage and power consumption when scheduler is idle
## Configuration
Override these macros in your build config if needed:
```ruby
conf.gem core: 'hal-win-task' do |spec|
# Custom tick interval (10ms instead of default 4ms)
spec.build.defines << 'MRB_TICK_UNIT=10'
# Custom timeslice (5 ticks instead of default 3)
spec.build.defines << 'MRB_TIMESLICE_TICK_COUNT=5'
# More concurrent VMs (16 instead of default 8)
spec.build.defines << 'MRB_TASK_MAX_VMS=16'
end
```
## Known Limitations
- Timer resolution typically limited to 1-2ms even with `timeBeginPeriod(1)`
- Multimedia timers consume system resources (kernel timer objects)
- Timer callbacks execute in separate thread context (handled internally)
- Not suitable for hard real-time requirements
- May interfere with other multimedia applications requesting different timer resolutions
## See Also
- `mruby-task` - Core task scheduler
- `hal-posix-task` - POSIX/Unix HAL implementation
- Task scheduler documentation: `mrbgems/mruby-task/README.md`
## Build Notes
The `winmm` library is automatically linked by the gem specification. No additional linker configuration is needed.
-11
View File
@@ -1,11 +0,0 @@
MRuby::Gem::Specification.new('hal-win-task') do |spec|
spec.license = 'MIT'
spec.authors = 'mruby developers'
spec.summary = 'Windows HAL for mruby-task'
# HAL gem depends on feature gem - brings in mruby-task automatically
spec.add_dependency 'mruby-task', core: 'mruby-task'
# Windows multimedia timer library
spec.linker.libraries << 'winmm'
end
+74
View File
@@ -0,0 +1,74 @@
# hw-adc - ADC peripheral interface for mruby
This gem provides the `ADC` class for reading analog input from
mruby. It is designed for embedded platforms such as ESP32 and
RP2040.
## Architecture
Platform-specific HAL implementations are in `ports/` directories:
- `ports/esp32/` - ESP32 using ESP-IDF ADC oneshot driver
- `ports/rp2040/` - RP2040 using Pico SDK ADC hardware
## Build Configuration
```ruby
MRuby::CrossBuild.new('esp32') do |conf|
conf.ports :esp32
conf.gem core: 'hw-adc'
end
```
## Ruby API
### ADC.new
```ruby
adc = ADC.new(pin)
```
- `pin` - ADC-capable GPIO pin number (Integer)
- Raises `ArgumentError` if the pin is not valid for ADC
### ADC#read / ADC#read_voltage
Read the analog value as voltage (Float).
```ruby
voltage = adc.read # => 1.65
voltage = adc.read_voltage # same
```
### ADC#read_raw
Read the raw ADC value (Integer, typically 0-4095 for 12-bit).
```ruby
raw = adc.read_raw # => 2048
```
### ADC#input
Returns the ADC input channel number assigned during initialization.
## HAL Interface
To add support for a new platform, create a `ports/<name>/`
directory and implement the following C functions declared in
`<mruby/adc.h>`:
```c
int mrb_adc_init(uint8_t pin);
uint32_t mrb_adc_read_raw(uint8_t input);
float mrb_adc_read_voltage(uint8_t input);
```
- `mrb_adc_init` returns the input channel number (>= 0) on
success, negative on error
- `input` parameter for read functions is the value returned by
`mrb_adc_init`
## License
MIT
+19
View File
@@ -0,0 +1,19 @@
#ifndef MRUBY_ADC_H
#define MRUBY_ADC_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* HAL functions - implemented in ports/<platform>/adc.c */
int mrb_adc_init(uint8_t pin);
uint32_t mrb_adc_read_raw(uint8_t input);
float mrb_adc_read_voltage(uint8_t input);
#ifdef __cplusplus
}
#endif
#endif /* MRUBY_ADC_H */
+5
View File
@@ -0,0 +1,5 @@
MRuby::Gem::Specification.new('hw-adc') do |spec|
spec.license = 'MIT'
spec.authors = ['HASUMI Hitoshi', 'mruby developers']
spec.summary = 'ADC peripheral interface'
end
+7
View File
@@ -0,0 +1,7 @@
class ADC
def initialize(pin)
@input = __init(pin)
end
attr_reader :input
end
+147
View File
@@ -0,0 +1,147 @@
#include <stdbool.h>
#include "esp_adc/adc_oneshot.h"
#include <mruby/adc.h>
#define VOLTAGE_MAX 3.3f
#define RESOLUTION 4095
#define UNIT_NUM 2
static adc_oneshot_unit_handle_t adc_handles[UNIT_NUM];
static bool adc_initialized;
static adc_unit_t
pin_to_unit(uint8_t pin)
{
switch (pin) {
#if CONFIG_IDF_TARGET_ESP32C3
case 0: case 1: case 2: case 3: case 4:
return ADC_UNIT_1;
case 5:
return ADC_UNIT_2;
#elif CONFIG_IDF_TARGET_ESP32
case 32: case 33: case 34: case 35: case 36: case 39:
return ADC_UNIT_1;
case 0: case 2: case 4: case 12: case 13: case 14: case 15:
case 25: case 26: case 27:
return ADC_UNIT_2;
#elif (CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32S2)
case 1: case 2: case 3: case 4: case 5:
case 6: case 7: case 8: case 9: case 10:
return ADC_UNIT_1;
case 11: case 12: case 13: case 14: case 15:
case 16: case 17: case 18: case 19: case 20:
return ADC_UNIT_2;
#endif
}
return -1;
}
static adc_channel_t
pin_to_channel(uint8_t pin)
{
switch (pin) {
#if CONFIG_IDF_TARGET_ESP32C3
case 0: return ADC_CHANNEL_0;
case 1: return ADC_CHANNEL_1;
case 2: return ADC_CHANNEL_2;
case 3: return ADC_CHANNEL_3;
case 4: return ADC_CHANNEL_4;
case 5: return ADC_CHANNEL_0;
#elif CONFIG_IDF_TARGET_ESP32
case 36: return ADC_CHANNEL_0;
case 39: return ADC_CHANNEL_3;
case 32: return ADC_CHANNEL_4;
case 33: return ADC_CHANNEL_5;
case 34: return ADC_CHANNEL_6;
case 35: return ADC_CHANNEL_7;
case 4: return ADC_CHANNEL_0;
case 0: return ADC_CHANNEL_1;
case 2: return ADC_CHANNEL_2;
case 15: return ADC_CHANNEL_3;
case 13: return ADC_CHANNEL_4;
case 12: return ADC_CHANNEL_5;
case 14: return ADC_CHANNEL_6;
case 27: return ADC_CHANNEL_7;
case 25: return ADC_CHANNEL_8;
case 26: return ADC_CHANNEL_9;
#elif (CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32S2)
case 1: return ADC_CHANNEL_0;
case 2: return ADC_CHANNEL_1;
case 3: return ADC_CHANNEL_2;
case 4: return ADC_CHANNEL_3;
case 5: return ADC_CHANNEL_4;
case 6: return ADC_CHANNEL_5;
case 7: return ADC_CHANNEL_6;
case 8: return ADC_CHANNEL_7;
case 9: return ADC_CHANNEL_8;
case 10: return ADC_CHANNEL_9;
case 11: return ADC_CHANNEL_0;
case 12: return ADC_CHANNEL_1;
case 13: return ADC_CHANNEL_2;
case 14: return ADC_CHANNEL_3;
case 15: return ADC_CHANNEL_4;
case 16: return ADC_CHANNEL_5;
case 17: return ADC_CHANNEL_6;
case 18: return ADC_CHANNEL_7;
case 19: return ADC_CHANNEL_8;
case 20: return ADC_CHANNEL_9;
#endif
}
return -1;
}
static int
init_units(void)
{
adc_unit_t units[] = { ADC_UNIT_1, ADC_UNIT_2 };
for (int i = 0; i < UNIT_NUM; i++) {
adc_oneshot_unit_init_cfg_t cfg = {
.unit_id = units[i],
.ulp_mode = ADC_ULP_MODE_DISABLE,
};
if (adc_oneshot_new_unit(&cfg, &adc_handles[i]) != ESP_OK)
return -1;
}
return 0;
}
int
mrb_adc_init(uint8_t pin)
{
if (!adc_initialized) {
if (init_units() != 0) return -1;
adc_initialized = true;
}
int ch = pin_to_channel(pin);
if (ch < 0) return -1;
adc_unit_t unit = pin_to_unit(pin);
if (unit < 0 || unit >= UNIT_NUM) return -1;
adc_oneshot_chan_cfg_t cfg = {
.atten = ADC_ATTEN_DB_12,
.bitwidth = ADC_BITWIDTH_DEFAULT,
};
if (adc_oneshot_config_channel(adc_handles[unit], ch, &cfg) != ESP_OK)
return -1;
return (int)pin;
}
uint32_t
mrb_adc_read_raw(uint8_t input)
{
adc_unit_t unit = pin_to_unit(input);
int ch = pin_to_channel(input);
int raw = 0;
if (unit >= 0 && unit < UNIT_NUM)
adc_oneshot_read(adc_handles[unit], ch, &raw);
return (uint32_t)raw;
}
float
mrb_adc_read_voltage(uint8_t input)
{
return (float)mrb_adc_read_raw(input) * VOLTAGE_MAX / RESOLUTION;
}
+43
View File
@@ -0,0 +1,43 @@
#include <stdbool.h>
#include "hardware/adc.h"
#include <mruby/adc.h>
#define VOLTAGE_MAX 3.3f
#define RESOLUTION 4095
#define TEMP_INPUT 4
static bool adc_initialized;
int
mrb_adc_init(uint8_t pin)
{
if (!adc_initialized) {
adc_init();
adc_initialized = true;
}
uint input;
switch (pin) {
case 26: input = 0; break;
case 27: input = 1; break;
case 28: input = 2; break;
case 29: input = 3; break;
default: return -1;
}
adc_gpio_init(pin);
return (int)input;
}
uint32_t
mrb_adc_read_raw(uint8_t input)
{
adc_select_input(input);
return (uint32_t)adc_read();
}
float
mrb_adc_read_voltage(uint8_t input)
{
adc_select_input(input);
return (float)adc_read() * VOLTAGE_MAX / RESOLUTION;
}
+48
View File
@@ -0,0 +1,48 @@
#include <mruby.h>
#include <mruby/presym.h>
#include <mruby/variable.h>
#include <mruby/adc.h>
/* ADC#__init(pin) */
static mrb_value
mrb_adc_m_init(mrb_state *mrb, mrb_value self)
{
mrb_int pin;
mrb_get_args(mrb, "i", &pin);
int input = mrb_adc_init((uint8_t)pin);
if (input < 0) {
mrb_raise(mrb, E_ARGUMENT_ERROR, "invalid ADC pin");
}
return mrb_fixnum_value(input);
}
/* ADC#read_raw */
static mrb_value
mrb_adc_m_read_raw(mrb_state *mrb, mrb_value self)
{
mrb_int input = mrb_integer(mrb_iv_get(mrb, self, MRB_IVSYM(input)));
return mrb_fixnum_value(mrb_adc_read_raw((uint8_t)input));
}
/* ADC#read_voltage (also aliased as read) */
static mrb_value
mrb_adc_m_read_voltage(mrb_state *mrb, mrb_value self)
{
mrb_int input = mrb_integer(mrb_iv_get(mrb, self, MRB_IVSYM(input)));
return mrb_float_value(mrb, (mrb_float)mrb_adc_read_voltage((uint8_t)input));
}
void
mrb_hw_adc_gem_init(mrb_state *mrb)
{
struct RClass *cls = mrb_define_class_id(mrb, MRB_SYM(ADC), mrb->object_class);
mrb_define_method_id(mrb, cls, MRB_SYM(__init), mrb_adc_m_init, MRB_ARGS_REQ(1));
mrb_define_method_id(mrb, cls, MRB_SYM(read_raw), mrb_adc_m_read_raw, MRB_ARGS_NONE());
mrb_define_method_id(mrb, cls, MRB_SYM(read_voltage), mrb_adc_m_read_voltage, MRB_ARGS_NONE());
mrb_define_method_id(mrb, cls, MRB_SYM(read), mrb_adc_m_read_voltage, MRB_ARGS_NONE());
}
void
mrb_hw_adc_gem_final(mrb_state *mrb)
{
}
+144
View File
@@ -0,0 +1,144 @@
# hw-gpio - GPIO peripheral interface for mruby
This gem provides the `GPIO` class for controlling general-purpose
input/output pins from mruby.
It is designed for embedded platforms such as ESP32 and RP2040.
## Architecture
Platform-specific HAL implementations are in `ports/` directories:
- `ports/esp32/` - ESP32 using ESP-IDF GPIO driver
- `ports/rp2040/` - RP2040 using Pico SDK
The build system automatically compiles matching port sources based
on `conf.ports` setting.
## Build Configuration
```ruby
# For ESP32
MRuby::CrossBuild.new('esp32') do |conf|
conf.ports :esp32
conf.gem core: 'hw-gpio'
end
# For RP2040
MRuby::CrossBuild.new('rp2040') do |conf|
conf.ports :rp2040
conf.gem core: 'hw-gpio'
end
```
## Ruby API
### Constants (direction/mode flags)
These flags can be combined with bitwise OR.
| Flag | Value | Description |
| ------------------ | ------ | -------------------------- |
| `GPIO::IN` | `0x01` | Input mode |
| `GPIO::OUT` | `0x02` | Output mode |
| `GPIO::HIGH_Z` | `0x04` | High-impedance (tri-state) |
| `GPIO::PULL_UP` | `0x08` | Enable pull-up resistor |
| `GPIO::PULL_DOWN` | `0x10` | Enable pull-down resistor |
| `GPIO::OPEN_DRAIN` | `0x20` | Enable open-drain output |
### GPIO.new
```ruby
gpio = GPIO.new(pin, flags)
```
- `pin` - GPIO pin number (Integer)
- `flags` - direction and mode flags (combined with `|`)
- Exactly one of `IN`, `OUT`, or `HIGH_Z` must be specified
- `PULL_UP` and `PULL_DOWN` are mutually exclusive
- Raises `ArgumentError` on invalid flag combinations
```ruby
# Input with pull-up
button = GPIO.new(2, GPIO::IN | GPIO::PULL_UP)
# Output
led = GPIO.new(25, GPIO::OUT)
```
### Instance Methods
#### GPIO#read
Read the current pin value.
```ruby
val = gpio.read # => 0 or 1
```
#### GPIO#write
Set the pin output value.
```ruby
gpio.write(1) # set high
gpio.write(0) # set low
```
- Raises `ArgumentError` if value is not 0 or 1
#### GPIO#high? / GPIO#low?
```ruby
gpio.high? # => true if read != 0
gpio.low? # => true if read == 0
```
#### GPIO#pin
```ruby
gpio.pin # => pin number passed to new
```
#### GPIO#setmode
Reconfigure pin direction and mode flags after initialization.
```ruby
gpio.setmode(GPIO::OUT)
```
### Class Methods
These operate directly on pin numbers without creating an instance.
```ruby
GPIO.read_at(pin) # => 0 or 1
GPIO.write_at(pin, val) # set pin output (0 or 1)
GPIO.set_dir_at(pin, flags) # set direction (IN/OUT/HIGH_Z)
GPIO.pull_up_at(pin) # enable pull-up
GPIO.pull_down_at(pin) # enable pull-down
GPIO.open_drain_at(pin) # enable open-drain
```
## HAL Interface
To add support for a new platform, create a `ports/<name>/`
directory and implement the following C functions declared in
`<mruby/gpio.h>`:
```c
void mrb_gpio_init(uint8_t pin);
void mrb_gpio_set_dir(uint8_t pin, uint8_t flags);
void mrb_gpio_pull_up(uint8_t pin);
void mrb_gpio_pull_down(uint8_t pin);
void mrb_gpio_open_drain(uint8_t pin);
int mrb_gpio_read(uint8_t pin);
void mrb_gpio_write(uint8_t pin, uint8_t val);
```
The port sources are compiled automatically when the build
configuration includes a matching `conf.ports` tag.
## License
MIT
+31
View File
@@ -0,0 +1,31 @@
#ifndef MRUBY_GPIO_H
#define MRUBY_GPIO_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* direction/mode flags (bitmask) */
#define MRB_GPIO_IN 0x01
#define MRB_GPIO_OUT 0x02
#define MRB_GPIO_HIGH_Z 0x04
#define MRB_GPIO_PULL_UP 0x08
#define MRB_GPIO_PULL_DOWN 0x10
#define MRB_GPIO_OPEN_DRAIN 0x20
/* HAL functions - implemented by hw-<platform>-gpio gems */
void mrb_gpio_init(uint8_t pin);
void mrb_gpio_set_dir(uint8_t pin, uint8_t flags);
void mrb_gpio_pull_up(uint8_t pin);
void mrb_gpio_pull_down(uint8_t pin);
void mrb_gpio_open_drain(uint8_t pin);
int mrb_gpio_read(uint8_t pin);
void mrb_gpio_write(uint8_t pin, uint8_t val);
#ifdef __cplusplus
}
#endif
#endif /* MRUBY_GPIO_H */
+5
View File
@@ -0,0 +1,5 @@
MRuby::Gem::Specification.new('hw-gpio') do |spec|
spec.license = 'MIT'
spec.authors = ['HASUMI Hitoshi', 'mruby developers']
spec.summary = 'GPIO peripheral interface'
end
+44
View File
@@ -0,0 +1,44 @@
class GPIO
IN = 0x01
OUT = 0x02
HIGH_Z = 0x04
PULL_UP = 0x08
PULL_DOWN = 0x10
OPEN_DRAIN = 0x20
attr_reader :pin
def initialize(pin, flags)
@pin = pin
__init(pin)
setmode(flags)
end
def setmode(flags)
dir = flags & (IN | OUT | HIGH_Z)
n = (flags & IN != 0 ? 1 : 0) + (flags & OUT != 0 ? 1 : 0) + (flags & HIGH_Z != 0 ? 1 : 0)
if n == 0
raise ArgumentError, "specify one of IN, OUT, or HIGH_Z"
elsif n > 1
raise ArgumentError, "IN, OUT, and HIGH_Z are exclusive"
end
GPIO.set_dir_at(@pin, dir)
pull = flags & (PULL_UP | PULL_DOWN)
if pull == (PULL_UP | PULL_DOWN)
raise ArgumentError, "PULL_UP and PULL_DOWN are exclusive"
end
GPIO.pull_up_at(@pin) if pull == PULL_UP
GPIO.pull_down_at(@pin) if pull == PULL_DOWN
GPIO.open_drain_at(@pin) if flags & OPEN_DRAIN != 0
nil
end
def high?
read != 0
end
def low?
read == 0
end
end
+50
View File
@@ -0,0 +1,50 @@
#include "driver/gpio.h"
#include <mruby/gpio.h>
void
mrb_gpio_init(uint8_t pin)
{
gpio_reset_pin(pin);
}
void
mrb_gpio_set_dir(uint8_t pin, uint8_t flags)
{
if (flags & MRB_GPIO_IN) {
gpio_set_direction(pin, GPIO_MODE_INPUT);
}
else if (flags & MRB_GPIO_OUT) {
gpio_set_direction(pin, GPIO_MODE_OUTPUT);
}
/* HIGH_Z: not yet implemented */
}
void
mrb_gpio_pull_up(uint8_t pin)
{
gpio_pullup_en(pin);
}
void
mrb_gpio_pull_down(uint8_t pin)
{
gpio_pulldown_en(pin);
}
void
mrb_gpio_open_drain(uint8_t pin)
{
/* not yet implemented */
}
int
mrb_gpio_read(uint8_t pin)
{
return gpio_get_level(pin);
}
void
mrb_gpio_write(uint8_t pin, uint8_t val)
{
gpio_set_level(pin, val);
}
+51
View File
@@ -0,0 +1,51 @@
#include <stdbool.h>
#include "hardware/gpio.h"
#include <mruby/gpio.h>
void
mrb_gpio_init(uint8_t pin)
{
gpio_init(pin);
}
void
mrb_gpio_set_dir(uint8_t pin, uint8_t flags)
{
if (flags & MRB_GPIO_IN) {
gpio_set_dir(pin, false);
}
else if (flags & MRB_GPIO_OUT) {
gpio_set_dir(pin, true);
}
/* HIGH_Z: not yet implemented */
}
void
mrb_gpio_pull_up(uint8_t pin)
{
gpio_pull_up(pin);
}
void
mrb_gpio_pull_down(uint8_t pin)
{
gpio_pull_down(pin);
}
void
mrb_gpio_open_drain(uint8_t pin)
{
/* not yet implemented */
}
int
mrb_gpio_read(uint8_t pin)
{
return gpio_get(pin);
}
void
mrb_gpio_write(uint8_t pin, uint8_t val)
{
gpio_put(pin, val == 1);
}
+119
View File
@@ -0,0 +1,119 @@
#include <mruby.h>
#include <mruby/presym.h>
#include <mruby/variable.h>
#include <mruby/gpio.h>
static mrb_value
mrb_gpio_m_init(mrb_state *mrb, mrb_value self)
{
mrb_int pin;
mrb_get_args(mrb, "i", &pin);
mrb_gpio_init((uint8_t)pin);
return mrb_nil_value();
}
/* GPIO.set_dir_at(pin, flags) */
static mrb_value
mrb_gpio_s_set_dir_at(mrb_state *mrb, mrb_value klass)
{
mrb_int pin, flags;
mrb_get_args(mrb, "ii", &pin, &flags);
mrb_gpio_set_dir((uint8_t)pin, (uint8_t)flags);
return mrb_nil_value();
}
/* GPIO.pull_up_at(pin) */
static mrb_value
mrb_gpio_s_pull_up_at(mrb_state *mrb, mrb_value klass)
{
mrb_int pin;
mrb_get_args(mrb, "i", &pin);
mrb_gpio_pull_up((uint8_t)pin);
return mrb_nil_value();
}
/* GPIO.pull_down_at(pin) */
static mrb_value
mrb_gpio_s_pull_down_at(mrb_state *mrb, mrb_value klass)
{
mrb_int pin;
mrb_get_args(mrb, "i", &pin);
mrb_gpio_pull_down((uint8_t)pin);
return mrb_nil_value();
}
/* GPIO.open_drain_at(pin) */
static mrb_value
mrb_gpio_s_open_drain_at(mrb_state *mrb, mrb_value klass)
{
mrb_int pin;
mrb_get_args(mrb, "i", &pin);
mrb_gpio_open_drain((uint8_t)pin);
return mrb_nil_value();
}
/* GPIO.read_at(pin) */
static mrb_value
mrb_gpio_s_read_at(mrb_state *mrb, mrb_value klass)
{
mrb_int pin;
mrb_get_args(mrb, "i", &pin);
return mrb_fixnum_value(mrb_gpio_read((uint8_t)pin));
}
/* GPIO.write_at(pin, val) */
static mrb_value
mrb_gpio_s_write_at(mrb_state *mrb, mrb_value klass)
{
mrb_int pin, val;
mrb_get_args(mrb, "ii", &pin, &val);
if (val != 0 && val != 1) {
mrb_raise(mrb, E_ARGUMENT_ERROR, "value must be 0 or 1");
}
mrb_gpio_write((uint8_t)pin, (uint8_t)val);
return mrb_nil_value();
}
/* GPIO#read */
static mrb_value
mrb_gpio_m_read(mrb_state *mrb, mrb_value self)
{
mrb_int pin = mrb_integer(mrb_iv_get(mrb, self, MRB_IVSYM(pin)));
return mrb_fixnum_value(mrb_gpio_read((uint8_t)pin));
}
/* GPIO#write(val) */
static mrb_value
mrb_gpio_m_write(mrb_state *mrb, mrb_value self)
{
mrb_int val;
mrb_get_args(mrb, "i", &val);
if (val != 0 && val != 1) {
mrb_raise(mrb, E_ARGUMENT_ERROR, "value must be 0 or 1");
}
mrb_int pin = mrb_integer(mrb_iv_get(mrb, self, MRB_IVSYM(pin)));
mrb_gpio_write((uint8_t)pin, (uint8_t)val);
return mrb_nil_value();
}
void
mrb_hw_gpio_gem_init(mrb_state *mrb)
{
struct RClass *cls = mrb_define_class_id(mrb, MRB_SYM(GPIO), mrb->object_class);
mrb_define_method_id(mrb, cls, MRB_SYM(__init), mrb_gpio_m_init, MRB_ARGS_REQ(1));
mrb_define_method_id(mrb, cls, MRB_SYM(read), mrb_gpio_m_read, MRB_ARGS_NONE());
mrb_define_method_id(mrb, cls, MRB_SYM(write), mrb_gpio_m_write, MRB_ARGS_REQ(1));
mrb_define_class_method_id(mrb, cls, MRB_SYM(set_dir_at), mrb_gpio_s_set_dir_at, MRB_ARGS_REQ(2));
mrb_define_class_method_id(mrb, cls, MRB_SYM(pull_up_at), mrb_gpio_s_pull_up_at, MRB_ARGS_REQ(1));
mrb_define_class_method_id(mrb, cls, MRB_SYM(pull_down_at), mrb_gpio_s_pull_down_at, MRB_ARGS_REQ(1));
mrb_define_class_method_id(mrb, cls, MRB_SYM(open_drain_at), mrb_gpio_s_open_drain_at, MRB_ARGS_REQ(1));
mrb_define_class_method_id(mrb, cls, MRB_SYM(read_at), mrb_gpio_s_read_at, MRB_ARGS_REQ(1));
mrb_define_class_method_id(mrb, cls, MRB_SYM(write_at), mrb_gpio_s_write_at, MRB_ARGS_REQ(2));
}
void
mrb_hw_gpio_gem_final(mrb_state *mrb)
{
}
+162
View File
@@ -0,0 +1,162 @@
# hw-i2c - I2C peripheral interface for mruby
This gem provides the `I2C` class for communicating with I2C devices from mruby. It is designed for embedded platforms such as ESP32 and RP2040.
## Architecture
Platform-specific HAL implementations are in `ports/` directories:
- `ports/esp32/` - ESP32 using ESP-IDF I2C master driver
- `ports/rp2040/` - RP2040 using Pico SDK
The build system automatically compiles matching port sources based
on `conf.ports` setting.
## Build Configuration
```ruby
# For ESP32
MRuby::CrossBuild.new('esp32') do |conf|
conf.ports :esp32
conf.gem core: 'hw-i2c'
end
# For RP2040
MRuby::CrossBuild.new('rp2040') do |conf|
conf.ports :rp2040
conf.gem core: 'hw-i2c'
end
```
## Ruby API
### I2C.new
```ruby
i2c = I2C.new(
unit: :ESP32_I2C0, # I2C unit name (platform-specific, required)
frequency: 100_000, # bus frequency in Hz (default: 100kHz)
sda_pin: 21, # SDA GPIO pin number (default: -1 for platform default)
scl_pin: 22, # SCL GPIO pin number (default: -1 for platform default)
timeout: 500 # default timeout in ms (default: 500)
)
```
#### Unit Names
| Platform | Available Units |
| -------- | ------------------------------ |
| ESP32 | `:ESP32_I2C0`, `:ESP32_I2C1` |
| RP2040 | `:RP2040_I2C0`, `:RP2040_I2C1` |
On RP2040, if `sda_pin` or `scl_pin` is -1, the Pico SDK default pins are used.
### I2C#write
Write data to an I2C device.
```ruby
i2c.write(addr, *data, timeout: 500)
```
- `addr` - 7-bit I2C device address (Integer)
- `data` - one or more data arguments, each can be:
- **Integer** - a single byte (0-255)
- **Array of Integer** - multiple bytes
- **String** - raw bytes
- `timeout:` - optional timeout in ms (overrides instance default)
- Returns the number of bytes written (Integer)
- Raises `IOError` on failure
```ruby
# Write a single byte
i2c.write(0x3C, 0x00)
# Write multiple bytes
i2c.write(0x3C, 0x00, [0xAE, 0xD5, 0x80])
# Write a string
i2c.write(0x3C, "hello")
# Mix data types
i2c.write(0x3C, 0x40, [0x01, 0x02], "data")
```
### I2C#read
Read data from an I2C device. Optionally write data before reading (repeated START).
```ruby
i2c.read(addr, length, *write_data, timeout: 500)
```
- `addr` - 7-bit I2C device address (Integer)
- `length` - number of bytes to read (Integer, must be positive)
- `write_data` - optional data to write before reading (same format as `write`). When provided, the gem performs a write-then-read transaction using I2C repeated START condition. This is the standard way to read from a specific register.
- `timeout:` - optional timeout in ms (overrides instance default)
- Returns the data read (String)
- Raises `IOError` on failure, `ArgumentError` if length <= 0
```ruby
# Simple read (2 bytes from device)
data = i2c.read(0x50, 2)
# Register read: write register address 0x00, then read 2 bytes
data = i2c.read(0x50, 2, 0x00)
# Multi-byte register address
data = i2c.read(0x50, 4, [0x00, 0x10])
```
### I2C#scan
Scan the I2C bus for responsive devices.
```ruby
i2c.scan(timeout: 500)
```
- `timeout:` - optional timeout per probe in ms
- Returns an Array of 7-bit addresses (Integer) that responded
```ruby
found = i2c.scan
# => [0x3C, 0x50, 0x68]
```
## HAL Interface
To add support for a new platform, create a `ports/<name>/`
directory and implement the following C functions declared in
`<mruby/i2c.h>`:
```c
int mrb_i2c_unit_name_to_num(const char *name);
mrb_i2c_status mrb_i2c_init(int unit, uint32_t freq, int8_t sda, int8_t scl);
int mrb_i2c_read(int unit, uint8_t addr, uint8_t *dst, size_t len,
uint32_t timeout_us);
int mrb_i2c_write(int unit, uint8_t addr, const uint8_t *src, size_t len,
uint32_t timeout_us);
int mrb_i2c_write_read(int unit, uint8_t addr,
const uint8_t *src, size_t wlen,
uint8_t *dst, size_t rlen,
uint32_t timeout_us);
```
The port sources are compiled automatically when the build
configuration includes a matching `conf.ports` tag.
### Error Codes
```c
typedef enum {
MRB_I2C_OK = 0,
MRB_I2C_ERROR_UNIT = -1, /* invalid or uninitialized unit */
MRB_I2C_ERROR_TIMEOUT = -2, /* communication timeout */
MRB_I2C_ERROR_NACK = -3, /* device did not acknowledge */
} mrb_i2c_status;
```
## License
MIT
+31
View File
@@ -0,0 +1,31 @@
#ifndef MRUBY_I2C_H
#define MRUBY_I2C_H
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
MRB_I2C_OK = 0,
MRB_I2C_ERROR_UNIT = -1,
MRB_I2C_ERROR_TIMEOUT = -2,
MRB_I2C_ERROR_NACK = -3,
} mrb_i2c_status;
/* HAL functions - implemented by hw-<platform>-i2c gems */
mrb_i2c_status mrb_i2c_init(int unit, uint32_t freq, int8_t sda, int8_t scl);
int mrb_i2c_read(int unit, uint8_t addr, uint8_t *dst, size_t len, uint32_t timeout_us);
int mrb_i2c_write(int unit, uint8_t addr, const uint8_t *src, size_t len, uint32_t timeout_us);
int mrb_i2c_write_read(int unit, uint8_t addr, const uint8_t *src, size_t wlen,
uint8_t *dst, size_t rlen, uint32_t timeout_us);
int mrb_i2c_unit_name_to_num(const char *name);
#ifdef __cplusplus
}
#endif
#endif /* MRUBY_I2C_H */
+5
View File
@@ -0,0 +1,5 @@
MRuby::Gem::Specification.new('hw-i2c') do |spec|
spec.license = 'MIT'
spec.authors = ['HASUMI Hitoshi', 'mruby developers']
spec.summary = 'I2C peripheral interface'
end
+21
View File
@@ -0,0 +1,21 @@
class I2C
DEFAULT_FREQUENCY = 100_000 # Hz
DEFAULT_TIMEOUT = 500 # ms
def initialize(unit:, frequency: DEFAULT_FREQUENCY, sda_pin: -1, scl_pin: -1, timeout: DEFAULT_TIMEOUT)
@timeout = timeout
@unit_num = __init(unit.to_s, frequency, sda_pin, scl_pin)
end
def scan(timeout: @timeout)
found = []
(0x08..0x77).each do |addr|
begin
read(addr, 1, timeout: timeout)
found << addr
rescue IOError
end
end
found
end
end
+117
View File
@@ -0,0 +1,117 @@
#include <string.h>
#include "driver/i2c_master.h"
#include <mruby/i2c.h>
typedef struct {
i2c_master_bus_handle_t bus;
uint32_t freq;
bool initialized;
} i2c_ctx;
/* ESP32 supports up to 2 I2C ports */
static i2c_ctx ctx[2];
static bool
valid_unit(int unit)
{
return unit >= 0 && unit <= 1 && ctx[unit].initialized;
}
static uint32_t
us_to_ms(uint32_t timeout_us)
{
uint32_t ms = (timeout_us + 999) / 1000;
return (ms < 10) ? 10 : ms;
}
static i2c_master_dev_handle_t
add_device(int unit, uint8_t addr)
{
i2c_device_config_t cfg = {
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
.device_address = addr,
.scl_speed_hz = ctx[unit].freq,
};
i2c_master_dev_handle_t dev;
if (i2c_master_bus_add_device(ctx[unit].bus, &cfg, &dev) != ESP_OK)
return NULL;
return dev;
}
int
mrb_i2c_unit_name_to_num(const char *name)
{
if (strcmp(name, "ESP32_I2C0") == 0) return 0;
if (strcmp(name, "ESP32_I2C1") == 0) return 1;
return MRB_I2C_ERROR_UNIT;
}
mrb_i2c_status
mrb_i2c_init(int unit, uint32_t freq, int8_t sda, int8_t scl)
{
if (unit < 0 || unit > 1) return MRB_I2C_ERROR_UNIT;
if (ctx[unit].initialized) {
i2c_del_master_bus(ctx[unit].bus);
ctx[unit].initialized = false;
}
i2c_master_bus_config_t cfg = {
.clk_source = I2C_CLK_SRC_DEFAULT,
.i2c_port = unit,
.scl_io_num = scl,
.sda_io_num = sda,
.glitch_ignore_cnt = 7,
.flags.enable_internal_pullup = true,
};
esp_err_t err = i2c_new_master_bus(&cfg, &ctx[unit].bus);
if (err != ESP_OK) return MRB_I2C_ERROR_UNIT;
ctx[unit].initialized = true;
ctx[unit].freq = freq;
return MRB_I2C_OK;
}
int
mrb_i2c_read(int unit, uint8_t addr, uint8_t *dst, size_t len,
uint32_t timeout_us)
{
if (!valid_unit(unit)) return MRB_I2C_ERROR_UNIT;
i2c_master_dev_handle_t dev = add_device(unit, addr);
if (!dev) return -1;
esp_err_t err = i2c_master_receive(dev, dst, len, us_to_ms(timeout_us));
i2c_master_bus_rm_device(dev);
return (err == ESP_OK) ? (int)len : -1;
}
int
mrb_i2c_write(int unit, uint8_t addr, const uint8_t *src, size_t len,
uint32_t timeout_us)
{
if (!valid_unit(unit)) return MRB_I2C_ERROR_UNIT;
i2c_master_dev_handle_t dev = add_device(unit, addr);
if (!dev) return -1;
esp_err_t err = i2c_master_transmit(dev, src, len, us_to_ms(timeout_us));
i2c_master_bus_rm_device(dev);
return (err == ESP_OK) ? (int)len : -1;
}
int
mrb_i2c_write_read(int unit, uint8_t addr, const uint8_t *src, size_t wlen,
uint8_t *dst, size_t rlen, uint32_t timeout_us)
{
if (!valid_unit(unit)) return MRB_I2C_ERROR_UNIT;
i2c_master_dev_handle_t dev = add_device(unit, addr);
if (!dev) return -1;
esp_err_t err = i2c_master_transmit_receive(dev, src, wlen, dst, rlen,
us_to_ms(timeout_us));
i2c_master_bus_rm_device(dev);
return (err == ESP_OK) ? (int)rlen : -1;
}
+64
View File
@@ -0,0 +1,64 @@
#include <string.h>
#include "pico/stdlib.h"
#include "hardware/i2c.h"
#include <mruby/i2c.h>
#define UNIT_SELECT(u) \
i2c_inst_t *inst; \
switch (u) { \
case 0: inst = i2c0; break; \
case 1: inst = i2c1; break; \
default: return MRB_I2C_ERROR_UNIT; \
}
int
mrb_i2c_unit_name_to_num(const char *name)
{
if (strcmp(name, "RP2040_I2C0") == 0) return 0;
if (strcmp(name, "RP2040_I2C1") == 0) return 1;
return MRB_I2C_ERROR_UNIT;
}
mrb_i2c_status
mrb_i2c_init(int unit, uint32_t freq, int8_t sda, int8_t scl)
{
UNIT_SELECT(unit);
i2c_init(inst, freq);
if (sda < 0) sda = PICO_DEFAULT_I2C_SDA_PIN;
if (scl < 0) scl = PICO_DEFAULT_I2C_SCL_PIN;
gpio_set_function(sda, GPIO_FUNC_I2C);
gpio_set_function(scl, GPIO_FUNC_I2C);
gpio_pull_up(sda);
gpio_pull_up(scl);
return MRB_I2C_OK;
}
int
mrb_i2c_read(int unit, uint8_t addr, uint8_t *dst, size_t len,
uint32_t timeout_us)
{
UNIT_SELECT(unit);
return i2c_read_timeout_us(inst, addr, dst, len, false, timeout_us);
}
int
mrb_i2c_write(int unit, uint8_t addr, const uint8_t *src, size_t len,
uint32_t timeout_us)
{
UNIT_SELECT(unit);
return i2c_write_timeout_us(inst, addr, src, len, false, timeout_us);
}
int
mrb_i2c_write_read(int unit, uint8_t addr, const uint8_t *src, size_t wlen,
uint8_t *dst, size_t rlen, uint32_t timeout_us)
{
UNIT_SELECT(unit);
/* write with nostop=true (no STOP, keeps bus for repeated START) */
int ret = i2c_write_timeout_us(inst, addr, src, wlen, true, timeout_us);
if (ret < 0) return ret;
/* read with nostop=false (STOP after read) */
return i2c_read_timeout_us(inst, addr, dst, rlen, false, timeout_us);
}
+200
View File
@@ -0,0 +1,200 @@
#include <string.h>
#include <mruby.h>
#include <mruby/presym.h>
#include <mruby/variable.h>
#include <mruby/array.h>
#include <mruby/string.h>
#include <mruby/i2c.h>
#define STACK_BUF_SIZE 256
#define E_IO_ERROR mrb_exc_get_id(mrb, MRB_SYM(IOError))
static size_t
i2c_fill_buf(mrb_state *mrb, uint8_t *buf, mrb_value *args, mrb_int argc)
{
size_t pos = 0;
for (mrb_int i = 0; i < argc; i++) {
switch (mrb_type(args[i])) {
case MRB_TT_ARRAY: {
mrb_int alen = RARRAY_LEN(args[i]);
const mrb_value *aptr = RARRAY_PTR(args[i]);
for (mrb_int j = 0; j < alen; j++) {
if (!mrb_integer_p(aptr[j])) {
mrb_raise(mrb, E_TYPE_ERROR, "array element must be Integer");
}
buf[pos++] = (uint8_t)mrb_integer(aptr[j]);
}
break;
}
case MRB_TT_INTEGER:
buf[pos++] = (uint8_t)mrb_integer(args[i]);
break;
case MRB_TT_STRING:
memcpy(&buf[pos], RSTRING_PTR(args[i]), RSTRING_LEN(args[i]));
pos += RSTRING_LEN(args[i]);
break;
default:
break;
}
}
return pos;
}
static size_t
i2c_calc_size(mrb_state *mrb, mrb_value *args, mrb_int argc)
{
size_t total = 0;
for (mrb_int i = 0; i < argc; i++) {
switch (mrb_type(args[i])) {
case MRB_TT_ARRAY:
total += RARRAY_LEN(args[i]);
break;
case MRB_TT_INTEGER:
total += 1;
break;
case MRB_TT_STRING:
total += RSTRING_LEN(args[i]);
break;
default:
mrb_raise(mrb, E_TYPE_ERROR, "Integer, Array, or String expected");
}
}
return total;
}
/* Allocate write buffer, fill it, return pointer and size.
Caller must free if need_free is set. */
static uint8_t*
i2c_build_buf(mrb_state *mrb, mrb_value *args, mrb_int argc,
size_t *out_len, uint8_t *sbuf, mrb_bool *need_free)
{
size_t total = i2c_calc_size(mrb, args, argc);
uint8_t *buf;
if (total <= STACK_BUF_SIZE) {
buf = sbuf;
*need_free = FALSE;
}
else {
buf = (uint8_t*)mrb_malloc(mrb, total);
*need_free = TRUE;
}
i2c_fill_buf(mrb, buf, args, argc);
*out_len = total;
return buf;
}
static mrb_int
get_timeout(mrb_state *mrb, mrb_value self, mrb_value kw)
{
if (mrb_undef_p(kw)) {
return mrb_integer(mrb_iv_get(mrb, self, MRB_IVSYM(timeout)));
}
return mrb_integer(kw);
}
static mrb_value
mrb_i2c_m_write(mrb_state *mrb, mrb_value self)
{
mrb_value *args;
mrb_int argc, addr;
const mrb_sym kw_names[] = { MRB_SYM(timeout) };
mrb_value kw_values[1];
mrb_kwargs kwargs = { 1, 0, kw_names, kw_values, NULL };
mrb_get_args(mrb, "i*:", &addr, &args, &argc, &kwargs);
mrb_int timeout_ms = get_timeout(mrb, self, kw_values[0]);
mrb_int unit = mrb_integer(mrb_iv_get(mrb, self, MRB_IVSYM(unit_num)));
uint8_t sbuf[STACK_BUF_SIZE];
size_t wlen;
mrb_bool need_free;
uint8_t *buf = i2c_build_buf(mrb, args, argc, &wlen, sbuf, &need_free);
int ret = mrb_i2c_write((int)unit, (uint8_t)addr, buf, wlen,
(uint32_t)timeout_ms * 1000);
if (need_free) mrb_free(mrb, buf);
if (ret < 0) {
mrb_raise(mrb, E_IO_ERROR, "I2C write failed");
}
return mrb_fixnum_value(ret);
}
static mrb_value
mrb_i2c_m_read(mrb_state *mrb, mrb_value self)
{
mrb_value *args;
mrb_int argc, addr, len;
const mrb_sym kw_names[] = { MRB_SYM(timeout) };
mrb_value kw_values[1];
mrb_kwargs kwargs = { 1, 0, kw_names, kw_values, NULL };
mrb_get_args(mrb, "ii*:", &addr, &len, &args, &argc, &kwargs);
if (len <= 0) {
mrb_raise(mrb, E_ARGUMENT_ERROR, "read length must be positive");
}
mrb_int timeout_ms = get_timeout(mrb, self, kw_values[0]);
mrb_int unit = mrb_integer(mrb_iv_get(mrb, self, MRB_IVSYM(unit_num)));
uint32_t timeout_us = (uint32_t)timeout_ms * 1000;
uint8_t *rxbuf = (uint8_t*)mrb_malloc(mrb, len);
int ret;
if (argc > 0) {
/* write-then-read (repeated START) */
uint8_t sbuf[STACK_BUF_SIZE];
size_t wlen;
mrb_bool need_free;
uint8_t *wbuf = i2c_build_buf(mrb, args, argc, &wlen, sbuf, &need_free);
ret = mrb_i2c_write_read((int)unit, (uint8_t)addr,
wbuf, wlen, rxbuf, (size_t)len, timeout_us);
if (need_free) mrb_free(mrb, wbuf);
}
else {
ret = mrb_i2c_read((int)unit, (uint8_t)addr, rxbuf, (size_t)len, timeout_us);
}
if (ret < 0) {
mrb_free(mrb, rxbuf);
mrb_raise(mrb, E_IO_ERROR, "I2C read failed");
}
mrb_value str = mrb_str_new(mrb, (const char*)rxbuf, ret);
mrb_free(mrb, rxbuf);
return str;
}
static mrb_value
mrb_i2c_m_init(mrb_state *mrb, mrb_value self)
{
const char *unit;
mrb_int freq, sda, scl;
mrb_get_args(mrb, "ziii", &unit, &freq, &sda, &scl);
int num = mrb_i2c_unit_name_to_num(unit);
if (num < 0) {
mrb_raisef(mrb, E_ARGUMENT_ERROR, "unknown I2C unit: %s", unit);
}
mrb_i2c_status st = mrb_i2c_init(num, (uint32_t)freq, (int8_t)sda, (int8_t)scl);
if (st != MRB_I2C_OK) {
mrb_raise(mrb, E_IO_ERROR, "I2C init failed");
}
return mrb_fixnum_value(num);
}
void
mrb_hw_i2c_gem_init(mrb_state *mrb)
{
struct RClass *cls = mrb_define_class_id(mrb, MRB_SYM(I2C), mrb->object_class);
mrb_define_method_id(mrb, cls, MRB_SYM(__init), mrb_i2c_m_init, MRB_ARGS_REQ(4));
mrb_define_method_id(mrb, cls, MRB_SYM(write), mrb_i2c_m_write, MRB_ARGS_REQ(1)|MRB_ARGS_REST()|MRB_ARGS_KEY(1, 0));
mrb_define_method_id(mrb, cls, MRB_SYM(read), mrb_i2c_m_read, MRB_ARGS_REQ(2)|MRB_ARGS_REST()|MRB_ARGS_KEY(1, 0));
}
void
mrb_hw_i2c_gem_final(mrb_state *mrb)
{
}
+87
View File
@@ -0,0 +1,87 @@
# hw-pwm - PWM peripheral interface for mruby
This gem provides the `PWM` class for Pulse Width Modulation
output from mruby. It is designed for embedded platforms such as
ESP32 and RP2040.
## Architecture
Platform-specific HAL implementations are in `ports/` directories:
- `ports/esp32/` - ESP32 using LEDC peripheral
- `ports/rp2040/` - RP2040 using Pico SDK PWM hardware
## Build Configuration
```ruby
MRuby::CrossBuild.new('esp32') do |conf|
conf.ports :esp32
conf.gem core: 'hw-pwm'
end
```
## Ruby API
### PWM.new
```ruby
pwm = PWM.new(pin, frequency: 1000, duty: 50)
```
- `pin` - GPIO pin number (Integer)
- `frequency:` - frequency in Hz (default: 0, disabled)
- `duty:` - duty cycle in percent 0-100 (default: 50)
### PWM#frequency(freq)
Set frequency in Hz. Returns the frequency. Setting 0 disables
output.
```ruby
pwm.frequency(1000) # 1 kHz
pwm.frequency(0) # disable
```
### PWM#period_us(us)
Set period in microseconds. Returns the corresponding frequency.
```ruby
pwm.period_us(1000) # 1ms period = 1 kHz
```
### PWM#duty(pct)
Set duty cycle in percent (0.0-100.0). Clamped to range.
```ruby
pwm.duty(75.0)
```
### PWM#pulse_width_us(us)
Set pulse width in microseconds. Duty cycle is calculated from
current frequency.
```ruby
pwm.pulse_width_us(500) # 500us pulse width
```
## HAL Interface
To add support for a new platform, create a `ports/<name>/`
directory and implement the following C functions declared in
`<mruby/pwm.h>`:
```c
void mrb_pwm_init(uint32_t pin);
void mrb_pwm_set_freq_duty(uint32_t pin, float frequency, float duty);
void mrb_pwm_set_enabled(uint32_t pin, bool enabled);
```
- `frequency` is in Hz, `duty` is in percent (0-100)
- `mrb_pwm_set_enabled` is called with `false` when frequency is 0
## License
MIT
+20
View File
@@ -0,0 +1,20 @@
#ifndef MRUBY_PWM_H
#define MRUBY_PWM_H
#include <stdint.h>
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
/* HAL functions - implemented in ports/<platform>/pwm.c */
void mrb_pwm_init(uint32_t pin);
void mrb_pwm_set_freq_duty(uint32_t pin, float frequency, float duty);
void mrb_pwm_set_enabled(uint32_t pin, bool enabled);
#ifdef __cplusplus
}
#endif
#endif /* MRUBY_PWM_H */
+5
View File
@@ -0,0 +1,5 @@
MRuby::Gem::Specification.new('hw-pwm') do |spec|
spec.license = 'MIT'
spec.authors = ['HASUMI Hitoshi', 'mruby developers']
spec.summary = 'PWM peripheral interface'
end
+9
View File
@@ -0,0 +1,9 @@
class PWM
def initialize(pin, frequency: 0, duty: 50)
@pin = pin
__init(@pin)
@frequency = frequency.to_f
@duty = duty.to_f
frequency(@frequency)
end
end
+59
View File
@@ -0,0 +1,59 @@
#include "driver/ledc.h"
#include <mruby/pwm.h>
#define DUTY_RESOLUTION LEDC_TIMER_14_BIT
static int8_t channel_for_gpio[GPIO_NUM_MAX];
static int next_channel;
void
mrb_pwm_init(uint32_t gpio)
{
if (gpio >= GPIO_NUM_MAX) return;
if (next_channel >= LEDC_CHANNEL_MAX) return;
ledc_timer_config_t timer_cfg = {
.speed_mode = LEDC_LOW_SPEED_MODE,
.timer_num = LEDC_TIMER_0,
.duty_resolution = DUTY_RESOLUTION,
.freq_hz = 1000,
.clk_cfg = LEDC_AUTO_CLK,
};
ledc_timer_config(&timer_cfg);
ledc_channel_config_t ch_cfg = {
.gpio_num = gpio,
.speed_mode = LEDC_LOW_SPEED_MODE,
.channel = next_channel,
.timer_sel = LEDC_TIMER_0,
.intr_type = LEDC_INTR_DISABLE,
.duty = 0,
.hpoint = 0,
};
ledc_channel_config(&ch_cfg);
channel_for_gpio[gpio] = next_channel++;
}
void
mrb_pwm_set_freq_duty(uint32_t gpio, float frequency, float duty)
{
if (gpio >= GPIO_NUM_MAX) return;
ledc_set_freq(LEDC_LOW_SPEED_MODE, LEDC_TIMER_0, (uint32_t)frequency);
int8_t ch = channel_for_gpio[gpio];
uint32_t max_duty = (1 << DUTY_RESOLUTION) - 1;
uint32_t d = (uint32_t)(duty * max_duty / 100.0f);
ledc_set_duty(LEDC_LOW_SPEED_MODE, ch, d);
ledc_update_duty(LEDC_LOW_SPEED_MODE, ch);
}
void
mrb_pwm_set_enabled(uint32_t gpio, bool enabled)
{
if (!enabled) {
int8_t ch = channel_for_gpio[gpio];
ledc_stop(LEDC_LOW_SPEED_MODE, ch, 0);
}
}
+33
View File
@@ -0,0 +1,33 @@
#include "pico/stdlib.h"
#include "hardware/pwm.h"
#include <mruby/pwm.h>
#define APB_CLK_FREQ 125000000
#define CLK_DIV 100.0f
void
mrb_pwm_init(uint32_t pin)
{
gpio_set_function(pin, GPIO_FUNC_PWM);
uint slice = pwm_gpio_to_slice_num(pin);
pwm_set_clkdiv(slice, CLK_DIV);
}
void
mrb_pwm_set_freq_duty(uint32_t pin, float frequency, float duty)
{
uint slice = pwm_gpio_to_slice_num(pin);
uint channel = pwm_gpio_to_channel(pin);
float period = 1.0f / frequency;
uint16_t wrap = (uint16_t)(period * APB_CLK_FREQ / CLK_DIV);
pwm_set_wrap(slice, wrap);
uint16_t level = (uint16_t)(wrap * duty / 100.0f);
pwm_set_chan_level(slice, channel, level);
}
void
mrb_pwm_set_enabled(uint32_t pin, bool enabled)
{
uint slice = pwm_gpio_to_slice_num(pin);
pwm_set_enabled(slice, enabled);
}
+90
View File
@@ -0,0 +1,90 @@
#include <mruby.h>
#include <mruby/presym.h>
#include <mruby/variable.h>
#include <mruby/pwm.h>
static mrb_value
mrb_pwm_m_init(mrb_state *mrb, mrb_value self)
{
mrb_int pin;
mrb_get_args(mrb, "i", &pin);
mrb_pwm_init((uint32_t)pin);
return mrb_nil_value();
}
static void
apply_freq_duty(mrb_state *mrb, mrb_value self)
{
uint32_t pin = (uint32_t)mrb_integer(mrb_iv_get(mrb, self, MRB_IVSYM(pin)));
mrb_float freq = mrb_as_float(mrb, mrb_iv_get(mrb, self, MRB_IVSYM(frequency)));
mrb_float duty = mrb_as_float(mrb, mrb_iv_get(mrb, self, MRB_IVSYM(duty)));
mrb_pwm_set_freq_duty(pin, (float)freq, (float)duty);
mrb_pwm_set_enabled(pin, freq > 0);
}
/* PWM#frequency(freq) */
static mrb_value
mrb_pwm_m_frequency(mrb_state *mrb, mrb_value self)
{
mrb_float freq;
mrb_get_args(mrb, "f", &freq);
mrb_iv_set(mrb, self, MRB_IVSYM(frequency), mrb_float_value(mrb, freq));
apply_freq_duty(mrb, self);
return mrb_float_value(mrb, freq);
}
/* PWM#period_us(us) */
static mrb_value
mrb_pwm_m_period_us(mrb_state *mrb, mrb_value self)
{
mrb_int us;
mrb_get_args(mrb, "i", &us);
mrb_float freq = 1000000.0 / us;
mrb_iv_set(mrb, self, MRB_IVSYM(frequency), mrb_float_value(mrb, freq));
apply_freq_duty(mrb, self);
return mrb_float_value(mrb, freq);
}
/* PWM#duty(pct) */
static mrb_value
mrb_pwm_m_duty(mrb_state *mrb, mrb_value self)
{
mrb_float duty;
mrb_get_args(mrb, "f", &duty);
if (duty < 0.0) duty = 0.0;
if (duty > 100.0) duty = 100.0;
mrb_iv_set(mrb, self, MRB_IVSYM(duty), mrb_float_value(mrb, duty));
apply_freq_duty(mrb, self);
return mrb_float_value(mrb, duty);
}
/* PWM#pulse_width_us(us) */
static mrb_value
mrb_pwm_m_pulse_width_us(mrb_state *mrb, mrb_value self)
{
mrb_int pw;
mrb_get_args(mrb, "i", &pw);
mrb_float freq = mrb_as_float(mrb, mrb_iv_get(mrb, self, MRB_IVSYM(frequency)));
mrb_float duty = (mrb_float)pw / 10000.0 * freq;
if (duty < 0.0) duty = 0.0;
if (duty > 100.0) duty = 100.0;
mrb_iv_set(mrb, self, MRB_IVSYM(duty), mrb_float_value(mrb, duty));
apply_freq_duty(mrb, self);
return mrb_float_value(mrb, duty);
}
void
mrb_hw_pwm_gem_init(mrb_state *mrb)
{
struct RClass *cls = mrb_define_class_id(mrb, MRB_SYM(PWM), mrb->object_class);
mrb_define_method_id(mrb, cls, MRB_SYM(__init), mrb_pwm_m_init, MRB_ARGS_REQ(1));
mrb_define_method_id(mrb, cls, MRB_SYM(frequency), mrb_pwm_m_frequency, MRB_ARGS_REQ(1));
mrb_define_method_id(mrb, cls, MRB_SYM(period_us), mrb_pwm_m_period_us, MRB_ARGS_REQ(1));
mrb_define_method_id(mrb, cls, MRB_SYM(duty), mrb_pwm_m_duty, MRB_ARGS_REQ(1));
mrb_define_method_id(mrb, cls, MRB_SYM(pulse_width_us), mrb_pwm_m_pulse_width_us, MRB_ARGS_REQ(1));
}
void
mrb_hw_pwm_gem_final(mrb_state *mrb)
{
}
+113
View File
@@ -0,0 +1,113 @@
# hw-spi - SPI peripheral interface for mruby
This gem provides the `SPI` class for Serial Peripheral Interface
communication from mruby. It is designed for embedded platforms
such as ESP32 and RP2040.
## Architecture
Platform-specific HAL implementations are in `ports/` directories:
- `ports/esp32/` - ESP32 using ESP-IDF SPI master driver
- `ports/rp2040/` - RP2040 using Pico SDK
The build system automatically compiles matching port sources based
on `conf.ports` setting.
## Build Configuration
```ruby
MRuby::CrossBuild.new('esp32') do |conf|
conf.ports :esp32
conf.gem core: 'hw-spi'
end
```
## Ruby API
### SPI.new
```ruby
spi = SPI.new(
unit: :RP2040_SPI0, # SPI unit name (required)
frequency: 100_000, # clock frequency in Hz (default: 100kHz)
sck_pin: -1, # SCK GPIO pin (default: platform default)
copi_pin: -1, # COPI/MOSI GPIO pin
cipo_pin: -1, # CIPO/MISO GPIO pin
cs_pin: -1, # CS GPIO pin (-1 for manual control)
mode: 0, # SPI mode 0-3 (default: 0)
first_bit: SPI::MSB_FIRST # bit order (default: MSB_FIRST)
)
```
#### Unit Names
| Platform | Available Units |
| -------- | ------------------------------------------ |
| ESP32 | `:ESP32_SPI2_HOST`, `:ESP32_HSPI_HOST`, |
| | `:ESP32_SPI3_HOST`\*, `:ESP32_VSPI_HOST`\* |
| RP2040 | `:RP2040_SPI0`, `:RP2040_SPI1` |
\*SPI3 availability depends on ESP32 variant.
### `SPI#write(*data)`
Write data to the SPI bus. Data can be Integer, Array, or String.
```ruby
spi.write(0x01, [0x02, 0x03])
```
### SPI#read(len, tx_value = 0)
Read `len` bytes. Optionally specify the value to transmit during
read.
```ruby
data = spi.read(4) # transmits 0x00 while reading
data = spi.read(4, 0xFF) # transmits 0xFF while reading
```
### `SPI#transfer(*data, additional_read_bytes: 0)`
Full-duplex transfer. Sends data and returns received bytes.
Use `additional_read_bytes:` to append zero-filled read bytes.
```ruby
# Send 1 byte command, read 4 bytes response
rx = spi.transfer(0x9F, additional_read_bytes: 4)
```
### SPI#select / SPI#deselect
Manually control the CS pin (when using GPIO-based chip select).
```ruby
spi.select do |s|
s.write(0x01)
data = s.read(4)
end # CS automatically deasserted
```
## HAL Interface
To add support for a new platform, create a `ports/<name>/`
directory and implement the following C functions declared in
`<mruby/spi.h>`:
```c
int mrb_spi_unit_name_to_num(const char *name);
mrb_spi_status mrb_spi_init(mrb_spi_info *info);
int mrb_spi_read(mrb_spi_info *info, uint8_t *dst, size_t len,
uint8_t tx_val);
int mrb_spi_write(mrb_spi_info *info, const uint8_t *src, size_t len);
int mrb_spi_transfer(mrb_spi_info *info, const uint8_t *tx,
uint8_t *rx, size_t len);
```
The `mrb_spi_info` struct contains all configuration (unit, pins,
frequency, mode, bit order) and is passed to every HAL call.
## License
MIT
+44
View File
@@ -0,0 +1,44 @@
#ifndef MRUBY_SPI_H
#define MRUBY_SPI_H
#include <stdint.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
#define MRB_SPI_MSB_FIRST 1
#define MRB_SPI_LSB_FIRST 0
typedef enum {
MRB_SPI_OK = 0,
MRB_SPI_ERROR_UNIT = -1,
MRB_SPI_ERROR_MODE = -2,
MRB_SPI_ERROR_FIRST_BIT = -3,
MRB_SPI_ERROR_INIT = -4,
} mrb_spi_status;
typedef struct {
uint32_t frequency;
uint8_t unit_num;
int8_t sck_pin;
int8_t copi_pin;
int8_t cipo_pin;
int8_t cs_pin;
uint8_t mode;
uint8_t first_bit;
} mrb_spi_info;
/* HAL functions - implemented in ports/<platform>/spi.c */
int mrb_spi_unit_name_to_num(const char *name);
mrb_spi_status mrb_spi_init(mrb_spi_info *info);
int mrb_spi_read(mrb_spi_info *info, uint8_t *dst, size_t len, uint8_t tx_val);
int mrb_spi_write(mrb_spi_info *info, const uint8_t *src, size_t len);
int mrb_spi_transfer(mrb_spi_info *info, const uint8_t *tx, uint8_t *rx, size_t len);
#ifdef __cplusplus
}
#endif
#endif /* MRUBY_SPI_H */
+5
View File
@@ -0,0 +1,5 @@
MRuby::Gem::Specification.new('hw-spi') do |spec|
spec.license = 'MIT'
spec.authors = ['HASUMI Hitoshi', 'mruby developers']
spec.summary = 'SPI peripheral interface'
end
+20
View File
@@ -0,0 +1,20 @@
class SPI
MSB_FIRST = 1
LSB_FIRST = 0
DEFAULT_FREQUENCY = 100_000
def select
@cs&.write 0
if block_given?
begin
yield self
ensure
deselect
end
end
end
def deselect
@cs&.write 1
end
end
+85
View File
@@ -0,0 +1,85 @@
#include <string.h>
#include "driver/spi_master.h"
#include <mruby/spi.h>
static spi_device_handle_t handles[SPI_HOST_MAX];
int
mrb_spi_unit_name_to_num(const char *name)
{
if (strcmp(name, "ESP32_SPI2_HOST") == 0) return SPI2_HOST;
if (strcmp(name, "ESP32_HSPI_HOST") == 0) return SPI2_HOST;
#if (SOC_SPI_PERIPH_NUM == 3)
if (strcmp(name, "ESP32_SPI3_HOST") == 0) return SPI3_HOST;
if (strcmp(name, "ESP32_VSPI_HOST") == 0) return SPI3_HOST;
#endif
return MRB_SPI_ERROR_UNIT;
}
mrb_spi_status
mrb_spi_init(mrb_spi_info *info)
{
if (handles[info->unit_num] != NULL) return MRB_SPI_OK;
spi_bus_config_t buscfg = {
.mosi_io_num = info->copi_pin,
.miso_io_num = info->cipo_pin,
.sclk_io_num = info->sck_pin,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
};
esp_err_t err = spi_bus_initialize(info->unit_num, &buscfg, SPI_DMA_CH_AUTO);
if (err != ESP_OK) return MRB_SPI_ERROR_INIT;
spi_device_interface_config_t devcfg = {
.clock_speed_hz = info->frequency,
.mode = info->mode,
.spics_io_num = info->cs_pin,
.queue_size = 7,
};
err = spi_bus_add_device(info->unit_num, &devcfg, &handles[info->unit_num]);
if (err != ESP_OK) {
spi_bus_free(info->unit_num);
handles[info->unit_num] = NULL;
return MRB_SPI_ERROR_INIT;
}
return MRB_SPI_OK;
}
int
mrb_spi_read(mrb_spi_info *info, uint8_t *dst, size_t len, uint8_t tx_val)
{
spi_transaction_t t = {
.length = len * 8,
.tx_buffer = NULL,
.rx_buffer = dst,
};
esp_err_t err = spi_device_polling_transmit(handles[info->unit_num], &t);
return (err == ESP_OK) ? (int)len : -1;
}
int
mrb_spi_write(mrb_spi_info *info, const uint8_t *src, size_t len)
{
spi_transaction_t t = {
.length = len * 8,
.tx_buffer = src,
.rx_buffer = NULL,
};
esp_err_t err = spi_device_polling_transmit(handles[info->unit_num], &t);
return (err == ESP_OK) ? (int)len : -1;
}
int
mrb_spi_transfer(mrb_spi_info *info, const uint8_t *tx, uint8_t *rx, size_t len)
{
spi_transaction_t t = {
.length = len * 8,
.tx_buffer = tx,
.rx_buffer = rx,
};
esp_err_t err = spi_device_polling_transmit(handles[info->unit_num], &t);
return (err == ESP_OK) ? (int)len : -1;
}
+73
View File
@@ -0,0 +1,73 @@
#include <string.h>
#include "pico/stdlib.h"
#include "hardware/spi.h"
#include <mruby/spi.h>
#define UNIT_SELECT(info) \
spi_inst_t *inst; \
switch ((info)->unit_num) { \
case 0: inst = spi0; break; \
case 1: inst = spi1; break; \
default: return MRB_SPI_ERROR_UNIT; \
}
int
mrb_spi_unit_name_to_num(const char *name)
{
if (strcmp(name, "RP2040_SPI0") == 0) return 0;
if (strcmp(name, "RP2040_SPI1") == 0) return 1;
return MRB_SPI_ERROR_UNIT;
}
mrb_spi_status
mrb_spi_init(mrb_spi_info *info)
{
UNIT_SELECT(info);
spi_init(inst, info->frequency);
if (info->sck_pin < 0) info->sck_pin = PICO_DEFAULT_SPI_SCK_PIN;
if (info->cipo_pin < 0) info->cipo_pin = PICO_DEFAULT_SPI_RX_PIN;
if (info->copi_pin < 0) info->copi_pin = PICO_DEFAULT_SPI_TX_PIN;
gpio_set_function(info->sck_pin, GPIO_FUNC_SPI);
gpio_set_function(info->cipo_pin, GPIO_FUNC_SPI);
gpio_set_function(info->copi_pin, GPIO_FUNC_SPI);
if (info->first_bit != MRB_SPI_MSB_FIRST) {
return MRB_SPI_ERROR_FIRST_BIT;
}
spi_cpol_t cpol;
spi_cpha_t cpha;
switch (info->mode) {
case 0: cpol = 0; cpha = 0; break;
case 1: cpol = 0; cpha = 1; break;
case 2: cpol = 1; cpha = 0; break;
case 3: cpol = 1; cpha = 1; break;
default: return MRB_SPI_ERROR_MODE;
}
spi_set_format(inst, 8, cpol, cpha, info->first_bit);
return MRB_SPI_OK;
}
int
mrb_spi_read(mrb_spi_info *info, uint8_t *dst, size_t len, uint8_t tx_val)
{
UNIT_SELECT(info);
return spi_read_blocking(inst, tx_val, dst, len);
}
int
mrb_spi_write(mrb_spi_info *info, const uint8_t *src, size_t len)
{
UNIT_SELECT(info);
return spi_write_blocking(inst, src, len);
}
int
mrb_spi_transfer(mrb_spi_info *info, const uint8_t *tx, uint8_t *rx, size_t len)
{
UNIT_SELECT(info);
return spi_write_read_blocking(inst, tx, rx, len);
}
+235
View File
@@ -0,0 +1,235 @@
#include <string.h>
#include <mruby.h>
#include <mruby/presym.h>
#include <mruby/variable.h>
#include <mruby/string.h>
#include <mruby/array.h>
#include <mruby/data.h>
#include <mruby/class.h>
#include <mruby/spi.h>
#define STACK_BUF_SIZE 256
#define E_IO_ERROR mrb_exc_get_id(mrb, MRB_SYM(IOError))
static void
spi_info_free(mrb_state *mrb, void *ptr)
{
mrb_free(mrb, ptr);
}
static const struct mrb_data_type spi_info_type = { "SPI", spi_info_free };
#define SPI_INFO(self) \
((mrb_spi_info*)mrb_data_get_ptr(mrb, self, &spi_info_type))
static size_t
spi_calc_size(mrb_state *mrb, mrb_value *args, mrb_int argc)
{
size_t total = 0;
for (mrb_int i = 0; i < argc; i++) {
switch (mrb_type(args[i])) {
case MRB_TT_ARRAY:
total += RARRAY_LEN(args[i]);
break;
case MRB_TT_INTEGER:
total += 1;
break;
case MRB_TT_STRING:
total += RSTRING_LEN(args[i]);
break;
default:
mrb_raise(mrb, E_TYPE_ERROR, "Integer, Array, or String expected");
}
}
return total;
}
static void
spi_fill_buf(mrb_state *mrb, uint8_t *buf, mrb_value *args, mrb_int argc,
size_t pad)
{
size_t pos = 0;
for (mrb_int i = 0; i < argc; i++) {
switch (mrb_type(args[i])) {
case MRB_TT_ARRAY: {
mrb_int alen = RARRAY_LEN(args[i]);
const mrb_value *aptr = RARRAY_PTR(args[i]);
for (mrb_int j = 0; j < alen; j++) {
if (!mrb_integer_p(aptr[j])) {
mrb_raise(mrb, E_TYPE_ERROR, "array element must be Integer");
}
buf[pos++] = (uint8_t)mrb_integer(aptr[j]);
}
break;
}
case MRB_TT_INTEGER:
buf[pos++] = (uint8_t)mrb_integer(args[i]);
break;
case MRB_TT_STRING:
memcpy(&buf[pos], RSTRING_PTR(args[i]), RSTRING_LEN(args[i]));
pos += RSTRING_LEN(args[i]);
break;
default:
break;
}
}
memset(&buf[pos], 0, pad);
}
/* SPI.new(unit:, frequency:, sck_pin:, cipo_pin:, copi_pin:,
cs_pin:, mode:, first_bit:) */
static mrb_value
mrb_spi_s_new(mrb_state *mrb, mrb_value klass)
{
const char *unit_name;
mrb_int freq = 100000, sck = -1, cipo = -1, copi = -1, cs = -1;
mrb_int mode = 0, first_bit = MRB_SPI_MSB_FIRST;
const mrb_sym kw_names[] = {
MRB_SYM(unit), MRB_SYM(frequency), MRB_SYM(sck_pin),
MRB_SYM(cipo_pin), MRB_SYM(copi_pin), MRB_SYM(cs_pin),
MRB_SYM(mode), MRB_SYM(first_bit)
};
mrb_value kw_values[8];
mrb_kwargs kwargs = { 8, 7, kw_names, kw_values, NULL };
mrb_get_args(mrb, ":", &kwargs);
/* unit is required */
if (mrb_undef_p(kw_values[0])) {
mrb_raise(mrb, E_ARGUMENT_ERROR, "unit: is required");
}
unit_name = mrb_str_to_cstr(mrb, mrb_sym_str(mrb, mrb_symbol(kw_values[0])));
if (!mrb_undef_p(kw_values[1])) freq = mrb_integer(kw_values[1]);
if (!mrb_undef_p(kw_values[2])) sck = mrb_integer(kw_values[2]);
if (!mrb_undef_p(kw_values[3])) cipo = mrb_integer(kw_values[3]);
if (!mrb_undef_p(kw_values[4])) copi = mrb_integer(kw_values[4]);
if (!mrb_undef_p(kw_values[5])) cs = mrb_integer(kw_values[5]);
if (!mrb_undef_p(kw_values[6])) mode = mrb_integer(kw_values[6]);
if (!mrb_undef_p(kw_values[7])) first_bit = mrb_integer(kw_values[7]);
int num = mrb_spi_unit_name_to_num(unit_name);
if (num < 0) {
mrb_raisef(mrb, E_ARGUMENT_ERROR, "unknown SPI unit: %s", unit_name);
}
mrb_spi_info *info = (mrb_spi_info*)mrb_malloc(mrb, sizeof(mrb_spi_info));
info->unit_num = (uint8_t)num;
info->frequency = (uint32_t)freq;
info->sck_pin = (int8_t)sck;
info->cipo_pin = (int8_t)cipo;
info->copi_pin = (int8_t)copi;
info->cs_pin = (int8_t)cs;
info->mode = (uint8_t)mode;
info->first_bit = (uint8_t)first_bit;
mrb_value self = mrb_obj_value(
Data_Wrap_Struct(mrb, mrb_class_ptr(klass), &spi_info_type, info));
mrb_spi_status st = mrb_spi_init(info);
if (st != MRB_SPI_OK) {
mrb_raise(mrb, E_IO_ERROR, "SPI init failed");
}
return self;
}
/* SPI#write(*data) */
static mrb_value
mrb_spi_m_write(mrb_state *mrb, mrb_value self)
{
mrb_value *args;
mrb_int argc;
mrb_get_args(mrb, "*", &args, &argc);
mrb_spi_info *info = SPI_INFO(self);
size_t total = spi_calc_size(mrb, args, argc);
if (total == 0) return mrb_fixnum_value(0);
uint8_t sbuf[STACK_BUF_SIZE];
uint8_t *buf = sbuf;
mrb_bool need_free = FALSE;
if (total > STACK_BUF_SIZE) {
buf = (uint8_t*)mrb_malloc(mrb, total);
need_free = TRUE;
}
spi_fill_buf(mrb, buf, args, argc, 0);
int ret = mrb_spi_write(info, buf, total);
if (need_free) mrb_free(mrb, buf);
if (ret < 0) mrb_raise(mrb, E_IO_ERROR, "SPI write failed");
return mrb_fixnum_value(ret);
}
/* SPI#read(len, tx_value=0) */
static mrb_value
mrb_spi_m_read(mrb_state *mrb, mrb_value self)
{
mrb_int len, tx_val = 0;
mrb_get_args(mrb, "i|i", &len, &tx_val);
if (len <= 0) mrb_raise(mrb, E_ARGUMENT_ERROR, "length must be positive");
mrb_spi_info *info = SPI_INFO(self);
uint8_t *buf = (uint8_t*)mrb_malloc(mrb, len);
int ret = mrb_spi_read(info, buf, (size_t)len, (uint8_t)tx_val);
if (ret < 0) {
mrb_free(mrb, buf);
mrb_raise(mrb, E_IO_ERROR, "SPI read failed");
}
mrb_value str = mrb_str_new(mrb, (const char*)buf, ret);
mrb_free(mrb, buf);
return str;
}
/* SPI#transfer(*data, additional_read_bytes: 0) */
static mrb_value
mrb_spi_m_transfer(mrb_state *mrb, mrb_value self)
{
mrb_value *args;
mrb_int argc, extra = 0;
const mrb_sym kw_names[] = { MRB_SYM(additional_read_bytes) };
mrb_value kw_values[1];
mrb_kwargs kwargs = { 1, 0, kw_names, kw_values, NULL };
mrb_get_args(mrb, "*:", &args, &argc, &kwargs);
if (!mrb_undef_p(kw_values[0])) extra = mrb_integer(kw_values[0]);
mrb_spi_info *info = SPI_INFO(self);
size_t total = spi_calc_size(mrb, args, argc) + (size_t)extra;
if (total == 0) return mrb_str_new(mrb, "", 0);
uint8_t sbuf_tx[STACK_BUF_SIZE], sbuf_rx[STACK_BUF_SIZE];
uint8_t *tx = sbuf_tx, *rx = sbuf_rx;
mrb_bool need_free = FALSE;
if (total > STACK_BUF_SIZE) {
tx = (uint8_t*)mrb_malloc(mrb, total * 2);
rx = tx + total;
need_free = TRUE;
}
spi_fill_buf(mrb, tx, args, argc, (size_t)extra);
int ret = mrb_spi_transfer(info, tx, rx, total);
if (ret < 0) {
if (need_free) mrb_free(mrb, tx);
mrb_raise(mrb, E_IO_ERROR, "SPI transfer failed");
}
mrb_value str = mrb_str_new(mrb, (const char*)rx, ret);
if (need_free) mrb_free(mrb, tx);
return str;
}
void
mrb_hw_spi_gem_init(mrb_state *mrb)
{
struct RClass *cls = mrb_define_class_id(mrb, MRB_SYM(SPI), mrb->object_class);
MRB_SET_INSTANCE_TT(cls, MRB_TT_CDATA);
mrb_define_class_method_id(mrb, cls, MRB_SYM(new), mrb_spi_s_new, MRB_ARGS_KEY(8, 1));
mrb_define_method_id(mrb, cls, MRB_SYM(write), mrb_spi_m_write, MRB_ARGS_REST());
mrb_define_method_id(mrb, cls, MRB_SYM(read), mrb_spi_m_read, MRB_ARGS_ARG(1, 1));
mrb_define_method_id(mrb, cls, MRB_SYM(transfer), mrb_spi_m_transfer, MRB_ARGS_REST()|MRB_ARGS_KEY(1, 0));
}
void
mrb_hw_spi_gem_final(mrb_state *mrb)
{
}
+181
View File
@@ -0,0 +1,181 @@
# hw-uart - UART peripheral interface for mruby
This gem provides the `UART` class for serial communication from mruby.
It is designed for embedded platforms such as ESP32 and RP2040.
## Architecture
Platform-specific HAL implementations are in `ports/` directories:
- `ports/esp32/` - ESP32 using ESP-IDF UART driver with FreeRTOS
task for RX
- `ports/rp2040/` - RP2040 using Pico SDK with IRQ-driven RX
Received data is buffered in a ring buffer (allocated by the common
gem) that the platform HAL populates via interrupt or task. The ring
buffer size must be a power of two.
## Build Configuration
```ruby
# For ESP32
MRuby::CrossBuild.new('esp32') do |conf|
conf.ports :esp32
conf.gem core: 'hw-uart'
end
# For RP2040
MRuby::CrossBuild.new('rp2040') do |conf|
conf.ports :rp2040
conf.gem core: 'hw-uart'
end
```
## Ruby API
### Constants
| Constant | Value | Description |
| ---------------------------- | ----- | ------------------ |
| `UART::PARITY_NONE` | `0` | No parity |
| `UART::PARITY_EVEN` | `1` | Even parity |
| `UART::PARITY_ODD` | `2` | Odd parity |
| `UART::FLOW_CONTROL_NONE` | `0` | No flow control |
| `UART::FLOW_CONTROL_RTS_CTS` | `1` | Hardware flow ctrl |
### UART.new
```ruby
uart = UART.new(
unit: :ESP32_UART1, # UART unit name (required)
tx_pin: 17, # TX GPIO pin (default: -1)
rx_pin: 16, # RX GPIO pin (default: -1)
baudrate: 9600, # baud rate (default: 9600)
data_bits: 8, # 5-8 (default: 8)
stop_bits: 1, # 1-2 (default: 1)
parity: UART::PARITY_NONE,
flow_control: UART::FLOW_CONTROL_NONE,
rx_buffer_size: 256 # must be power of two (default: 256)
)
```
#### Unit Names
| Platform | Available Units |
| -------- | ------------------------------------------------ |
| ESP32 | `:ESP32_UART0`, `:ESP32_UART1`, `:ESP32_UART2`\* |
| RP2040 | `:RP2040_UART0`, `:RP2040_UART1` |
\*UART2 availability depends on ESP32 variant.
### Instance Methods
#### UART#write(str)
Write a string to the UART. Returns number of bytes written.
```ruby
uart.write("Hello\r\n")
```
#### UART#read(len = nil)
Read from the RX buffer. Returns `nil` if no data is available.
- Without argument: returns all available data
- With `len`: returns exactly `len` bytes, or `nil` if fewer are
available
```ruby
data = uart.read # all available
data = uart.read(10) # exactly 10 bytes or nil
```
#### UART#readpartial(maxlen)
Read up to `maxlen` bytes from the RX buffer. Returns `nil` if empty.
```ruby
data = uart.readpartial(64)
```
#### UART#gets
Read a line (up to and including `"\n"`). Returns `nil` if no
complete line is available.
```ruby
line = uart.gets
```
#### UART#bytes_available
Returns the number of bytes in the RX buffer.
```ruby
n = uart.bytes_available
```
#### UART#puts(str)
Write string with line ending appended (if not already present).
```ruby
uart.puts("Hello") # writes "Hello\n"
```
#### UART#flush
Wait for all TX data to be sent.
#### UART#clear_rx_buffer / UART#clear_tx_buffer
Discard buffered data.
#### UART#send_break(duration_ms = 100)
Send a UART break signal for the specified duration.
#### UART#setmode(baudrate:, data_bits:, stop_bits:, parity:, flow_control:)
Reconfigure UART parameters after initialization. All parameters are
optional.
#### UART#baudrate
Returns the current baud rate.
#### UART#line_ending=(ending)
Set the line ending used by `puts`. Must be `"\n"`, `"\r"`, or
`"\r\n"`.
## HAL Interface
To add support for a new platform, create a `ports/<name>/`
directory and implement the following C functions declared in
`<mruby/uart.h>`:
```c
int mrb_uart_unit_name_to_num(const char *name);
mrb_uart_status mrb_uart_init(int unit, uint32_t tx_pin, uint32_t rx_pin,
mrb_uart_ringbuf *rxbuf);
uint32_t mrb_uart_set_baudrate(int unit, uint32_t baudrate);
void mrb_uart_set_format(int unit, uint32_t data_bits,
uint32_t stop_bits, uint8_t parity);
void mrb_uart_set_flow_control(int unit, bool cts, bool rts);
void mrb_uart_write(int unit, const uint8_t *src, size_t len);
void mrb_uart_flush(int unit);
void mrb_uart_send_break(int unit, uint32_t duration_ms);
void mrb_uart_clear_rx(int unit);
void mrb_uart_clear_tx(int unit);
```
The `rxbuf` parameter passed to `mrb_uart_init` is a ring buffer
allocated by the common gem. The platform must arrange for received
bytes to be pushed into it using `mrb_uart_ringbuf_push()` (e.g.,
from an interrupt handler or RTOS task).
## License
MIT
+60
View File
@@ -0,0 +1,60 @@
#ifndef MRUBY_UART_H
#define MRUBY_UART_H
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
#define MRB_UART_PARITY_NONE 0
#define MRB_UART_PARITY_EVEN 1
#define MRB_UART_PARITY_ODD 2
#define MRB_UART_FLOW_NONE 0
#define MRB_UART_FLOW_RTS_CTS 1
typedef enum {
MRB_UART_OK = 0,
MRB_UART_ERROR_UNIT = -1,
} mrb_uart_status;
/* Ring buffer for interrupt-driven RX.
Allocated by common gem, populated by platform interrupt handler.
size must be a power of two. */
typedef struct {
volatile int head;
volatile int tail;
int mask;
uint8_t data[];
} mrb_uart_ringbuf;
/* Ring buffer helpers (implemented in hw-uart/src/ringbuf.c) */
bool mrb_uart_ringbuf_init(mrb_uart_ringbuf *rb, int size);
bool mrb_uart_ringbuf_push(mrb_uart_ringbuf *rb, uint8_t ch);
int mrb_uart_ringbuf_pop(mrb_uart_ringbuf *rb, uint8_t *dst, int len);
int mrb_uart_ringbuf_available(const mrb_uart_ringbuf *rb);
void mrb_uart_ringbuf_clear(mrb_uart_ringbuf *rb);
int mrb_uart_ringbuf_search(const mrb_uart_ringbuf *rb, uint8_t ch);
/* HAL functions - implemented by hw-<platform>-uart gems */
int mrb_uart_unit_name_to_num(const char *name);
mrb_uart_status mrb_uart_init(int unit, uint32_t tx_pin, uint32_t rx_pin,
mrb_uart_ringbuf *rxbuf);
uint32_t mrb_uart_set_baudrate(int unit, uint32_t baudrate);
void mrb_uart_set_format(int unit, uint32_t data_bits, uint32_t stop_bits,
uint8_t parity);
void mrb_uart_set_flow_control(int unit, bool cts, bool rts);
void mrb_uart_write(int unit, const uint8_t *src, size_t len);
void mrb_uart_flush(int unit);
void mrb_uart_send_break(int unit, uint32_t duration_ms);
void mrb_uart_clear_rx(int unit);
void mrb_uart_clear_tx(int unit);
#ifdef __cplusplus
}
#endif
#endif /* MRUBY_UART_H */
+5
View File
@@ -0,0 +1,5 @@
MRuby::Gem::Specification.new('hw-uart') do |spec|
spec.license = 'MIT'
spec.authors = ['HASUMI Hitoshi', 'mruby developers']
spec.summary = 'UART peripheral interface'
end
+54
View File
@@ -0,0 +1,54 @@
class UART
PARITY_NONE = 0
PARITY_EVEN = 1
PARITY_ODD = 2
FLOW_CONTROL_NONE = 0
FLOW_CONTROL_RTS_CTS = 1
attr_reader :baudrate
def initialize(unit:, tx_pin: -1, rx_pin: -1, baudrate: 9600,
data_bits: 8, stop_bits: 1, parity: PARITY_NONE,
flow_control: FLOW_CONTROL_NONE, rx_buffer_size: 256)
__open_rx_buffer(rx_buffer_size)
@unit_num = __open_connection(unit.to_s, tx_pin, rx_pin)
@baudrate = __set_baudrate(baudrate)
__set_format(data_bits, stop_bits, parity)
set_flow_control(flow_control)
@line_ending = "\n"
end
def setmode(baudrate: nil, data_bits: nil, stop_bits: nil,
parity: nil, flow_control: nil)
@baudrate = __set_baudrate(baudrate) if baudrate
__set_format(data_bits || 8, stop_bits || 1, parity || PARITY_NONE)
set_flow_control(flow_control || FLOW_CONTROL_NONE)
self
end
def line_ending=(ending)
unless ["\n", "\r", "\r\n"].include?(ending)
raise ArgumentError, "invalid line ending"
end
@line_ending = ending
end
def puts(str)
write str
write @line_ending unless str.end_with?(@line_ending)
nil
end
private
def set_flow_control(mode)
case mode
when FLOW_CONTROL_NONE
__set_flow_control(false, false)
when FLOW_CONTROL_RTS_CTS
__set_flow_control(true, true)
else
raise ArgumentError, "invalid flow control mode"
end
end
end
+147
View File
@@ -0,0 +1,147 @@
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "driver/uart.h"
#include <mruby/uart.h>
#define RX_TASK_BUF_SIZE 128
#define QUEUE_LENGTH 20
#define TASK_STACK_SIZE 4096
#define TASK_PRIORITY 12
typedef struct {
int unit;
QueueHandle_t queue;
mrb_uart_ringbuf *rxbuf;
} uart_ctx;
static uart_ctx ctx[UART_NUM_MAX];
static void
rx_task(void *arg)
{
uart_ctx *c = (uart_ctx*)arg;
uart_event_t event;
uint8_t buf[RX_TASK_BUF_SIZE];
for (;;) {
if (xQueueReceive(c->queue, &event, portMAX_DELAY)) {
if (event.type == UART_DATA) {
size_t n = event.size > RX_TASK_BUF_SIZE ? RX_TASK_BUF_SIZE : event.size;
uart_read_bytes(c->unit, buf, n, portMAX_DELAY);
for (size_t i = 0; i < n; i++) {
mrb_uart_ringbuf_push(c->rxbuf, buf[i]);
}
}
}
}
}
int
mrb_uart_unit_name_to_num(const char *name)
{
if (strcmp(name, "ESP32_UART0") == 0) return UART_NUM_0;
if (strcmp(name, "ESP32_UART1") == 0) return UART_NUM_1;
#ifdef UART_NUM_2
if (strcmp(name, "ESP32_UART2") == 0) return UART_NUM_2;
#endif
return MRB_UART_ERROR_UNIT;
}
mrb_uart_status
mrb_uart_init(int unit, uint32_t tx_pin, uint32_t rx_pin,
mrb_uart_ringbuf *rxbuf)
{
if (unit < 0 || unit >= UART_NUM_MAX) return MRB_UART_ERROR_UNIT;
uart_config_t cfg = {
.baud_rate = 9600,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.source_clk = UART_SCLK_DEFAULT,
};
int bufsize = (rxbuf->mask + 1);
uart_driver_install(unit, bufsize, 0, QUEUE_LENGTH, &ctx[unit].queue, 0);
uart_param_config(unit, &cfg);
uart_set_pin(unit, tx_pin, rx_pin, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
ctx[unit].unit = unit;
ctx[unit].rxbuf = rxbuf;
char name[32];
snprintf(name, sizeof(name), "uart_rx_%d", unit);
xTaskCreate(rx_task, name, TASK_STACK_SIZE, &ctx[unit], TASK_PRIORITY, NULL);
return MRB_UART_OK;
}
uint32_t
mrb_uart_set_baudrate(int unit, uint32_t baudrate)
{
uart_set_baudrate(unit, baudrate);
return baudrate;
}
void
mrb_uart_set_format(int unit, uint32_t data_bits, uint32_t stop_bits,
uint8_t parity)
{
static const uart_word_length_t wl[] = {
UART_DATA_5_BITS, UART_DATA_6_BITS, UART_DATA_7_BITS, UART_DATA_8_BITS
};
static const uart_stop_bits_t sb[] = {
UART_STOP_BITS_1, UART_STOP_BITS_2
};
static const uart_parity_t pr[] = {
UART_PARITY_DISABLE, UART_PARITY_EVEN, UART_PARITY_ODD
};
if (data_bits >= 5 && data_bits <= 8)
uart_set_word_length(unit, wl[data_bits - 5]);
if (stop_bits >= 1 && stop_bits <= 2)
uart_set_stop_bits(unit, sb[stop_bits - 1]);
if (parity <= 2)
uart_set_parity(unit, pr[parity]);
}
void
mrb_uart_set_flow_control(int unit, bool cts, bool rts)
{
uart_hw_flowcontrol_t mode = UART_HW_FLOWCTRL_DISABLE;
if (cts && rts) mode = UART_HW_FLOWCTRL_CTS_RTS;
else if (cts) mode = UART_HW_FLOWCTRL_CTS;
else if (rts) mode = UART_HW_FLOWCTRL_RTS;
uart_set_hw_flow_ctrl(unit, mode, 122);
}
void
mrb_uart_write(int unit, const uint8_t *src, size_t len)
{
uart_write_bytes(unit, (const char*)src, len);
}
void
mrb_uart_flush(int unit)
{
uart_wait_tx_done(unit, 100);
}
void
mrb_uart_send_break(int unit, uint32_t duration_ms)
{
uart_write_bytes_with_break(unit, NULL, 0, duration_ms);
}
void
mrb_uart_clear_rx(int unit)
{
uart_flush_input(unit);
}
void
mrb_uart_clear_tx(int unit)
{
/* not supported on ESP-IDF */
}
+136
View File
@@ -0,0 +1,136 @@
#include <string.h>
#include "pico/stdlib.h"
#include "hardware/gpio.h"
#include "hardware/uart.h"
#include "hardware/irq.h"
#include <mruby/uart.h>
#define UNIT_SELECT(u) \
uart_inst_t *inst; \
switch (u) { \
case 0: inst = uart0; break; \
case 1: inst = uart1; break; \
default: return MRB_UART_ERROR_UNIT; \
}
/* void-returning variant for functions that can't return error */
#define UNIT_SELECT_V(u) \
uart_inst_t *inst; \
switch (u) { \
case 0: inst = uart0; break; \
case 1: inst = uart1; break; \
default: return; \
}
static mrb_uart_ringbuf *rx_bufs[2];
static void
on_uart0_rx(void)
{
while (uart_is_readable(uart0)) {
mrb_uart_ringbuf_push(rx_bufs[0], uart_getc(uart0));
}
}
static void
on_uart1_rx(void)
{
while (uart_is_readable(uart1)) {
mrb_uart_ringbuf_push(rx_bufs[1], uart_getc(uart1));
}
}
int
mrb_uart_unit_name_to_num(const char *name)
{
if (strcmp(name, "RP2040_UART0") == 0) return 0;
if (strcmp(name, "RP2040_UART1") == 0) return 1;
return MRB_UART_ERROR_UNIT;
}
mrb_uart_status
mrb_uart_init(int unit, uint32_t tx_pin, uint32_t rx_pin,
mrb_uart_ringbuf *rxbuf)
{
UNIT_SELECT(unit);
uart_init(inst, 9600);
gpio_set_function(tx_pin, GPIO_FUNC_UART);
gpio_set_function(rx_pin, GPIO_FUNC_UART);
rx_bufs[unit] = rxbuf;
uint irq;
if (unit == 0) {
irq = UART0_IRQ;
irq_set_exclusive_handler(irq, on_uart0_rx);
}
else {
irq = UART1_IRQ;
irq_set_exclusive_handler(irq, on_uart1_rx);
}
irq_set_enabled(irq, true);
uart_set_irq_enables(inst, true, false);
return MRB_UART_OK;
}
uint32_t
mrb_uart_set_baudrate(int unit, uint32_t baudrate)
{
UNIT_SELECT(unit);
return uart_set_baudrate(inst, baudrate);
}
void
mrb_uart_set_format(int unit, uint32_t data_bits, uint32_t stop_bits,
uint8_t parity)
{
UNIT_SELECT_V(unit);
uart_set_format(inst, data_bits, stop_bits, (uart_parity_t)parity);
}
void
mrb_uart_set_flow_control(int unit, bool cts, bool rts)
{
UNIT_SELECT_V(unit);
uart_set_hw_flow(inst, cts, rts);
}
void
mrb_uart_write(int unit, const uint8_t *src, size_t len)
{
UNIT_SELECT_V(unit);
uart_write_blocking(inst, src, len);
}
void
mrb_uart_flush(int unit)
{
UNIT_SELECT_V(unit);
uart_tx_wait_blocking(inst);
}
void
mrb_uart_send_break(int unit, uint32_t duration_ms)
{
UNIT_SELECT_V(unit);
uart_set_break(inst, true);
sleep_ms(duration_ms);
uart_set_break(inst, false);
}
void
mrb_uart_clear_rx(int unit)
{
UNIT_SELECT_V(unit);
while (uart_is_readable(inst)) {
uart_getc(inst);
}
}
void
mrb_uart_clear_tx(int unit)
{
/* not supported on RP2040 */
}
+59
View File
@@ -0,0 +1,59 @@
#include <mruby/uart.h>
bool
mrb_uart_ringbuf_init(mrb_uart_ringbuf *rb, int size)
{
/* size must be a power of two */
if (size <= 0 || (size & (size - 1)) != 0) return false;
rb->head = 0;
rb->tail = 0;
rb->mask = size - 1;
return true;
}
bool
mrb_uart_ringbuf_push(mrb_uart_ringbuf *rb, uint8_t ch)
{
int next = (rb->head + 1) & rb->mask;
if (next == rb->tail) return false; /* full */
rb->data[rb->head] = ch;
rb->head = next;
return true;
}
int
mrb_uart_ringbuf_pop(mrb_uart_ringbuf *rb, uint8_t *dst, int len)
{
int i;
for (i = 0; i < len; i++) {
if (rb->tail == rb->head) break; /* empty */
dst[i] = rb->data[rb->tail];
rb->tail = (rb->tail + 1) & rb->mask;
}
return i;
}
int
mrb_uart_ringbuf_available(const mrb_uart_ringbuf *rb)
{
return (rb->head - rb->tail) & rb->mask;
}
void
mrb_uart_ringbuf_clear(mrb_uart_ringbuf *rb)
{
rb->tail = rb->head;
}
int
mrb_uart_ringbuf_search(const mrb_uart_ringbuf *rb, uint8_t ch)
{
int pos = rb->tail;
int i = 0;
while (pos != rb->head) {
if (rb->data[pos] == ch) return i;
pos = (pos + 1) & rb->mask;
i++;
}
return -1;
}
+236
View File
@@ -0,0 +1,236 @@
#include <mruby.h>
#include <mruby/presym.h>
#include <mruby/variable.h>
#include <mruby/string.h>
#include <mruby/data.h>
#include <mruby/class.h>
#include <mruby/uart.h>
#define E_IO_ERROR mrb_exc_get_id(mrb, MRB_SYM(IOError))
#define DEFAULT_RX_BUF_SIZE 256
static void
rxbuf_free(mrb_state *mrb, void *ptr)
{
mrb_free(mrb, ptr);
}
static const struct mrb_data_type rxbuf_type = { "UART", rxbuf_free };
/* UART#__open_rx_buffer(size) */
static mrb_value
mrb_uart_m_open_rxbuf(mrb_state *mrb, mrb_value self)
{
mrb_int size;
mrb_get_args(mrb, "i", &size);
if (size <= 0) size = DEFAULT_RX_BUF_SIZE;
mrb_uart_ringbuf *rb = (mrb_uart_ringbuf*)mrb_malloc(mrb,
sizeof(mrb_uart_ringbuf) + sizeof(uint8_t) * size);
if (!mrb_uart_ringbuf_init(rb, (int)size)) {
mrb_free(mrb, rb);
mrb_raise(mrb, E_ARGUMENT_ERROR, "rx_buffer_size must be a power of two");
}
DATA_PTR(self) = rb;
DATA_TYPE(self) = &rxbuf_type;
return mrb_nil_value();
}
/* UART#__open_connection(unit_name, tx_pin, rx_pin) */
static mrb_value
mrb_uart_m_open_conn(mrb_state *mrb, mrb_value self)
{
const char *name;
mrb_int tx_pin, rx_pin;
mrb_get_args(mrb, "zii", &name, &tx_pin, &rx_pin);
int num = mrb_uart_unit_name_to_num(name);
if (num < 0) {
mrb_raisef(mrb, E_ARGUMENT_ERROR, "unknown UART unit: %s", name);
}
mrb_uart_ringbuf *rb = (mrb_uart_ringbuf*)mrb_data_get_ptr(mrb, self, &rxbuf_type);
mrb_uart_status st = mrb_uart_init(num, (uint32_t)tx_pin, (uint32_t)rx_pin, rb);
if (st != MRB_UART_OK) {
mrb_raise(mrb, E_IO_ERROR, "UART init failed");
}
return mrb_fixnum_value(num);
}
/* UART#__set_baudrate(baud) */
static mrb_value
mrb_uart_m_set_baudrate(mrb_state *mrb, mrb_value self)
{
mrb_int baud;
mrb_get_args(mrb, "i", &baud);
mrb_int unit = mrb_integer(mrb_iv_get(mrb, self, MRB_IVSYM(unit_num)));
uint32_t actual = mrb_uart_set_baudrate((int)unit, (uint32_t)baud);
return mrb_fixnum_value(actual);
}
/* UART#__set_format(data_bits, stop_bits, parity) */
static mrb_value
mrb_uart_m_set_format(mrb_state *mrb, mrb_value self)
{
mrb_int data_bits, stop_bits, parity;
mrb_get_args(mrb, "iii", &data_bits, &stop_bits, &parity);
mrb_int unit = mrb_integer(mrb_iv_get(mrb, self, MRB_IVSYM(unit_num)));
mrb_uart_set_format((int)unit, (uint32_t)data_bits, (uint32_t)stop_bits, (uint8_t)parity);
return mrb_nil_value();
}
/* UART#__set_flow_control(cts, rts) */
static mrb_value
mrb_uart_m_set_flow(mrb_state *mrb, mrb_value self)
{
mrb_bool cts, rts;
mrb_get_args(mrb, "bb", &cts, &rts);
mrb_int unit = mrb_integer(mrb_iv_get(mrb, self, MRB_IVSYM(unit_num)));
mrb_uart_set_flow_control((int)unit, cts, rts);
return mrb_nil_value();
}
/* UART#write(str) */
static mrb_value
mrb_uart_m_write(mrb_state *mrb, mrb_value self)
{
mrb_value str;
mrb_get_args(mrb, "S", &str);
mrb_int unit = mrb_integer(mrb_iv_get(mrb, self, MRB_IVSYM(unit_num)));
size_t len = RSTRING_LEN(str);
mrb_uart_write((int)unit, (const uint8_t*)RSTRING_PTR(str), len);
return mrb_fixnum_value(len);
}
/* UART#read(len=nil) */
static mrb_value
mrb_uart_m_read(mrb_state *mrb, mrb_value self)
{
mrb_int len = -1;
mrb_get_args(mrb, "|i", &len);
mrb_uart_ringbuf *rb = (mrb_uart_ringbuf*)mrb_data_get_ptr(mrb, self, &rxbuf_type);
int avail = mrb_uart_ringbuf_available(rb);
if (avail == 0) return mrb_nil_value();
if (len >= 0) {
if (avail < len) return mrb_nil_value();
avail = (int)len;
}
uint8_t *buf = (uint8_t*)mrb_malloc(mrb, avail);
int n = mrb_uart_ringbuf_pop(rb, buf, avail);
mrb_value str = mrb_str_new(mrb, (const char*)buf, n);
mrb_free(mrb, buf);
return str;
}
/* UART#readpartial(maxlen) */
static mrb_value
mrb_uart_m_readpartial(mrb_state *mrb, mrb_value self)
{
mrb_int maxlen;
mrb_get_args(mrb, "i", &maxlen);
mrb_uart_ringbuf *rb = (mrb_uart_ringbuf*)mrb_data_get_ptr(mrb, self, &rxbuf_type);
int avail = mrb_uart_ringbuf_available(rb);
if (avail == 0) return mrb_nil_value();
if (avail > maxlen) avail = (int)maxlen;
uint8_t *buf = (uint8_t*)mrb_malloc(mrb, avail);
int n = mrb_uart_ringbuf_pop(rb, buf, avail);
mrb_value str = mrb_str_new(mrb, (const char*)buf, n);
mrb_free(mrb, buf);
return str;
}
/* UART#bytes_available */
static mrb_value
mrb_uart_m_bytes_available(mrb_state *mrb, mrb_value self)
{
mrb_uart_ringbuf *rb = (mrb_uart_ringbuf*)mrb_data_get_ptr(mrb, self, &rxbuf_type);
return mrb_fixnum_value(mrb_uart_ringbuf_available(rb));
}
/* UART#gets */
static mrb_value
mrb_uart_m_gets(mrb_state *mrb, mrb_value self)
{
mrb_uart_ringbuf *rb = (mrb_uart_ringbuf*)mrb_data_get_ptr(mrb, self, &rxbuf_type);
int pos = mrb_uart_ringbuf_search(rb, (uint8_t)'\n');
if (pos < 0) return mrb_nil_value();
int len = pos + 1;
uint8_t *buf = (uint8_t*)mrb_malloc(mrb, len);
mrb_uart_ringbuf_pop(rb, buf, len);
mrb_value str = mrb_str_new(mrb, (const char*)buf, len);
mrb_free(mrb, buf);
return str;
}
/* UART#flush */
static mrb_value
mrb_uart_m_flush(mrb_state *mrb, mrb_value self)
{
mrb_int unit = mrb_integer(mrb_iv_get(mrb, self, MRB_IVSYM(unit_num)));
mrb_uart_flush((int)unit);
return self;
}
/* UART#clear_tx_buffer */
static mrb_value
mrb_uart_m_clear_tx(mrb_state *mrb, mrb_value self)
{
mrb_int unit = mrb_integer(mrb_iv_get(mrb, self, MRB_IVSYM(unit_num)));
mrb_uart_clear_tx((int)unit);
return self;
}
/* UART#clear_rx_buffer */
static mrb_value
mrb_uart_m_clear_rx(mrb_state *mrb, mrb_value self)
{
mrb_uart_ringbuf *rb = (mrb_uart_ringbuf*)mrb_data_get_ptr(mrb, self, &rxbuf_type);
mrb_uart_ringbuf_clear(rb);
mrb_int unit = mrb_integer(mrb_iv_get(mrb, self, MRB_IVSYM(unit_num)));
mrb_uart_clear_rx((int)unit);
return self;
}
/* UART#send_break(duration_ms=100) */
static mrb_value
mrb_uart_m_send_break(mrb_state *mrb, mrb_value self)
{
mrb_int ms = 100;
mrb_get_args(mrb, "|i", &ms);
mrb_int unit = mrb_integer(mrb_iv_get(mrb, self, MRB_IVSYM(unit_num)));
mrb_uart_send_break((int)unit, (uint32_t)ms);
return self;
}
void
mrb_hw_uart_gem_init(mrb_state *mrb)
{
struct RClass *cls = mrb_define_class_id(mrb, MRB_SYM(UART), mrb->object_class);
MRB_SET_INSTANCE_TT(cls, MRB_TT_CDATA);
mrb_define_method_id(mrb, cls, MRB_SYM(__open_rx_buffer), mrb_uart_m_open_rxbuf, MRB_ARGS_REQ(1));
mrb_define_method_id(mrb, cls, MRB_SYM(__open_connection), mrb_uart_m_open_conn, MRB_ARGS_REQ(3));
mrb_define_method_id(mrb, cls, MRB_SYM(__set_baudrate), mrb_uart_m_set_baudrate, MRB_ARGS_REQ(1));
mrb_define_method_id(mrb, cls, MRB_SYM(__set_format), mrb_uart_m_set_format, MRB_ARGS_REQ(3));
mrb_define_method_id(mrb, cls, MRB_SYM(__set_flow_control), mrb_uart_m_set_flow, MRB_ARGS_REQ(2));
mrb_define_method_id(mrb, cls, MRB_SYM(write), mrb_uart_m_write, MRB_ARGS_REQ(1));
mrb_define_method_id(mrb, cls, MRB_SYM(read), mrb_uart_m_read, MRB_ARGS_OPT(1));
mrb_define_method_id(mrb, cls, MRB_SYM(readpartial), mrb_uart_m_readpartial, MRB_ARGS_REQ(1));
mrb_define_method_id(mrb, cls, MRB_SYM(bytes_available), mrb_uart_m_bytes_available, MRB_ARGS_NONE());
mrb_define_method_id(mrb, cls, MRB_SYM(gets), mrb_uart_m_gets, MRB_ARGS_NONE());
mrb_define_method_id(mrb, cls, MRB_SYM(flush), mrb_uart_m_flush, MRB_ARGS_NONE());
mrb_define_method_id(mrb, cls, MRB_SYM(clear_tx_buffer), mrb_uart_m_clear_tx, MRB_ARGS_NONE());
mrb_define_method_id(mrb, cls, MRB_SYM(clear_rx_buffer), mrb_uart_m_clear_rx, MRB_ARGS_NONE());
mrb_define_method_id(mrb, cls, MRB_SYM(send_break), mrb_uart_m_send_break, MRB_ARGS_OPT(1));
}
void
mrb_hw_uart_gem_final(mrb_state *mrb)
{
}
+12 -64
View File
@@ -450,27 +450,7 @@ class Array
# a.permutation(0).to_a #=> [[]] # one permutation of length 0
# a.permutation(4).to_a #=> [] # no permutations of length 4
def permutation(n=self.size, &block)
n = n.__to_int
return to_enum(:permutation, n) unless block
size = self.size
if n == 0
yield []
elsif 0 < n && n <= size
i = 0
while i<size
result = [self[i]]
if n-1 > 0
ary = self[0...i] + self[i+1..-1]
ary.permutation(n-1) do |c|
yield result + c
end
else
yield result
end
i += 1
end
end
self
__combination(:permutation, n, &block)
end
##
@@ -497,28 +477,7 @@ class Array
# a.combination(5).to_a #=> [] # no combinations of length 5
def combination(n, &block)
n = n.__to_int
return to_enum(:combination, n) unless block
size = self.size
if n == 0
yield []
elsif n == 1
i = 0
while i<size
yield [self[i]]
i += 1
end
elsif n <= size
i = 0
while i<size
result = [self[i]]
self[i+1..-1].combination(n-1) do |c|
yield result + c
end
i += 1
end
end
self
__combination(:combination, n, &block)
end
##
@@ -642,9 +601,7 @@ class Array
# a = [1, 2, 3]
# a.repeated_combination(2).to_a #=> [[1,1],[1,2],[1,3],[2,2],[2,3],[3,3]]
def repeated_combination(n, &block)
raise TypeError, "no implicit conversion into Integer" unless 0 <=> n
return to_enum(:repeated_combination, n) unless block
__repeated_combination(n, false, &block)
__combination(:repeated_combination, n, &block)
end
##
@@ -667,36 +624,27 @@ class Array
# a = [1, 2]
# a.repeated_permutation(2).to_a #=> [[1,1],[1,2],[2,1],[2,2]]
def repeated_permutation(n, &block)
n = n.__to_int
raise TypeError, "no implicit conversion into Integer" unless 0 <=> n
return to_enum(:repeated_permutation, n) unless block
__repeated_combination(n, true, &block)
__combination(:repeated_permutation, n, &block)
end
def __repeated_combination(n, permutation, &block)
n = n.__to_int
case n
def __combination(mode, k, &block)
k = k.__to_int
return to_enum(mode, k) unless block
case k
when 0
yield []
when 1
# Keep fast Ruby path for n=1
# Keep fast Ruby path for k=1
i = 0
while i < self.size
yield [self[i]]
i += 1
end
else
if n > 0
if state = __combination_init(mode, k)
# Use C iterator for complex cases
state = __combination_init(n, permutation)
while (indices = __combination_next(state))
# Convert indices to elements in Ruby
tmp = [nil] * n
i = 0
while i < n
tmp[i] = self[indices[i]]
i += 1
end
while tmp = __combination_next(state)
yield tmp
end
end
+122 -57
View File
@@ -32,10 +32,8 @@ typedef khash_t(ary_set) ary_set_t;
/* Combination state structure for repeated_combination optimization */
struct mrb_combination_state {
mrb_int *indices;
mrb_int n;
mrb_int array_size;
mrb_bool permutation;
mrb_bool finished;
mrb_int n, k; /* nPk, nCk */
int mode;
};
static void
@@ -1533,6 +1531,13 @@ ary_deconstruct(mrb_state *mrb, mrb_value ary)
return ary;
}
enum {
comb_finished = 0,
comb_repeated_permutation = 1,
comb_repeated_combination = 2,
comb_permutation = 3,
comb_combination = 4
};
/*
* Internal method to initialize combination state.
@@ -1541,33 +1546,74 @@ ary_deconstruct(mrb_state *mrb, mrb_value ary)
static mrb_value
ary_combination_init(mrb_state *mrb, mrb_value self)
{
mrb_int n;
mrb_bool permutation;
mrb_int k;
mrb_sym mode_sym;
mrb_get_args(mrb, "ib", &n, &permutation);
mrb_get_args(mrb, "ni", &mode_sym, &k);
#if MRB_INT_MAX > SIZE_MAX
if (n > SIZE_MAX) {
if (k > SIZE_MAX) {
mrb_raise(mrb, E_ARGUMENT_ERROR, "number too large");
}
#endif
if (k < 1 || RARRAY_LEN(self) < 1) {
return mrb_nil_value();
}
int mode;
switch (mode_sym) {
case MRB_SYM(repeated_permutation):
mode = comb_repeated_permutation;
break;
case MRB_SYM(repeated_combination):
mode = comb_repeated_combination;
break;
case MRB_SYM(permutation):
if (k > RARRAY_LEN(self)) {
return mrb_nil_value();
}
mode = comb_permutation;
break;
case MRB_SYM(combination):
if (k > RARRAY_LEN(self)) {
return mrb_nil_value();
}
mode = comb_combination;
break;
default:
mrb_raise(mrb, E_ARGUMENT_ERROR, "wrong mode");
}
struct RData *d;
struct mrb_combination_state *state;
Data_Make_Struct(mrb, mrb->object_class, struct mrb_combination_state,
&mrb_combination_state_type, state, d);
state->n = n;
state->array_size = RARRAY_LEN(self);
state->permutation = permutation;
state->finished = (n <= 0 && n != 0);
state->k = k;
state->n = RARRAY_LEN(self);
state->mode = mode;
state->indices = (mrb_int*)mrb_calloc(mrb, k, sizeof(mrb_int));
if (n > 0) {
state->indices = (mrb_int*)mrb_calloc(mrb, n, sizeof(mrb_int));
if (mode == comb_permutation || mode == comb_combination) {
for (mrb_int i = 0; i < k; i++) {
state->indices[i] = i;
}
}
return mrb_obj_value(d);
}
static void
adjust_next_permutation_index(struct mrb_combination_state *state, mrb_int i)
{
for (mrb_int j = i - 1; j >= 0; j--) {
if (state->indices[i] == state->indices[j]) {
state->indices[i]++;
j = i;
}
}
}
/*
* Internal method to get next combination as index array.
* Returns array of indices or nil when iteration is complete.
@@ -1575,68 +1621,87 @@ ary_combination_init(mrb_state *mrb, mrb_value self)
static mrb_value
ary_combination_next(mrb_state *mrb, mrb_value self)
{
mrb_value state_obj;
mrb_get_args(mrb, "o", &state_obj);
struct mrb_combination_state *state;
/* Validate state object type and get data */
state = (struct mrb_combination_state*)mrb_data_check_and_get(mrb, state_obj, &mrb_combination_state_type);
if (!state) {
mrb_raise(mrb, E_ARGUMENT_ERROR, "invalid combination state");
}
mrb_get_args(mrb, "d", &state, &mrb_combination_state_type);
/* Check if iteration is complete */
if (state->finished) return mrb_nil_value();
if (state->mode == comb_finished) return mrb_nil_value();
/* Validate array hasn't been modified during iteration */
if (RARRAY_LEN(self) != state->array_size) {
if (RARRAY_LEN(self) != state->n) {
mrb_raise(mrb, E_RUNTIME_ERROR, "array modified during iteration");
}
/* Edge case: empty array */
if (state->array_size == 0) {
state->finished = TRUE;
return mrb_nil_value();
}
/* Validate current indices are still in bounds */
for (mrb_int i = 0; i < state->n; i++) {
if (state->indices[i] >= state->array_size) {
state->finished = TRUE;
for (mrb_int i = 0; i < state->k; i++) {
if (state->indices[i] >= state->n) {
state->mode = comb_finished;
mrb_free(mrb, state->indices);
state->indices = NULL;
return mrb_nil_value();
}
}
/* Build current combination indices */
mrb_value result = mrb_ary_new_capa(mrb, state->n);
for (mrb_int i = 0; i < state->n; i++) {
mrb_ary_push(mrb, result, mrb_fixnum_value(state->indices[i]));
/* Build current combination */
mrb_value result = mrb_ary_new_capa(mrb, state->k);
const mrb_value *p = RARRAY_PTR(self);
for (mrb_int i = 0; i < state->k; i++) {
mrb_ary_push(mrb, result, p[state->indices[i]]);
}
mrb_int pos = state->n - 1;
while (pos >= 0) {
state->indices[pos]++;
if (state->indices[pos] < state->array_size) break;
pos--;
}
if (pos < 0) {
state->finished = TRUE;
}
else {
/* Reset dependent indices */
for (mrb_int i = pos + 1; i < state->n; i++) {
if (state->permutation) {
state->indices[i] = 0;
}
else {
state->indices[i] = state->indices[i - 1];
switch (state->mode) {
case comb_repeated_permutation:
case comb_repeated_combination:
for (mrb_int i = state->k - 1; i >= 0; i--) {
state->indices[i]++;
if (state->indices[i] < state->n) {
/* Reset dependent indices */
mrb_int reset = (state->mode == comb_repeated_permutation) ? 0 : state->indices[i];
for (i++; i < state->k; i++) {
state->indices[i] = reset;
}
return result;
}
}
break;
case comb_permutation:
for (mrb_int i = state->k - 1; i >= 0; i--) {
state->indices[i]++;
// adjust so that it does not overlap with the leading index
adjust_next_permutation_index(state, i);
if (state->indices[i] < state->n) {
// adjust all trailing indexes to complete the function
for (i++; i < state->k; i++) {
state->indices[i] = 0;
adjust_next_permutation_index(state, i);
}
return result;
}
}
break;
case comb_combination:
for (mrb_int i = state->k - 1; i >= 0; i--) {
state->indices[i]++;
if (state->indices[i] <= state->n - state->k + i) {
// replace each overflowed indices with an index incremented by 1 from the previous one
for (i++; i < state->k; i++) {
state->indices[i] = state->indices[i - 1] + 1;
}
return result;
}
}
break;
default: // it probably wont happen, but just in case
result = mrb_nil_value();
break;
}
state->mode = comb_finished;
mrb_free(mrb, state->indices);
state->indices = NULL;
return result;
}
+64 -18
View File
@@ -50,11 +50,15 @@ typedef struct mpz_context {
mpz_pool_t *pool; /* NULL for heap-only operations */
} mpz_ctx_t;
/* Convenience macros for context creation */
/* Convenience macros for context creation.
* Uses positional aggregate initialization instead of a C99 compound
* literal with designated initializers, so the file compiles as C++
* on legacy toolchains (pre-C++20). Member order must match the
* mpz_context struct declaration above. */
#define MPZ_CTX_INIT(mrb_ptr, ctx, pool_ptr) \
mpz_pool_t pool ## _storage = {{0}};\
mpz_pool_t *pool_ptr = &pool ## _storage;\
mpz_ctx_t ctx ## _struct = ((mpz_ctx_t){.mrb = (mrb_ptr), .pool = (pool_ptr)}); \
mpz_ctx_t ctx ## _struct = { (mrb_ptr), (pool_ptr) }; \
mpz_ctx_t *ctx = &(ctx ## _struct);
/* Access macros for readability */
@@ -344,6 +348,14 @@ mpz_move(mpz_ctx_t *ctx, mpz_t *y, mpz_t *x)
x->sz = 0;
}
static inline void
mpz_swap(mpz_t *a, mpz_t *b)
{
mpz_t tmp = *a;
*a = *b;
*b = tmp;
}
static size_t
digits(mpz_t *x)
{
@@ -361,6 +373,8 @@ trim(mpz_t *x)
while (x->sz && x->p[x->sz-1] == 0) {
x->sz--;
}
/* Maintain invariant: sz == 0 implies sn == 0 (zero is canonical). */
if (x->sz == 0) x->sn = 0;
}
/* z = x + y, without regard for sign */
@@ -1236,7 +1250,8 @@ mpn_add_var(mp_limb *rp, const mp_limb *ap, size_t an,
rp[i] = LOW(sum);
carry = HIGH(sum);
}
} else {
}
else {
for (; i < bn; i++) {
mp_dbl_limb sum = (mp_dbl_limb)bp[i] + carry;
rp[i] = LOW(sum);
@@ -1560,7 +1575,8 @@ mpz_mul_toom3(mpz_ctx_t *ctx, mp_limb *result,
mpn_add(t2, t2, w_len, w0, w_len);
mpn_add(t2, t2, w_len, winf, w_len);
mpn_neg(t2, t2, w_len);
} else {
}
else {
mpn_sub(t2, t2, w_len, w0, w_len);
mpn_sub(t2, t2, w_len, winf, w_len);
}
@@ -3133,8 +3149,11 @@ mpz_mod(mpz_ctx_t *ctx, mpz_t *r, mpz_t *x, mpz_t *y)
return;
}
/* Barrett reduction for moderate-sized moduli (>= 4 limbs where setup is worthwhile) */
if (y->sz >= 4 && y->sz <= 16 && x->sz >= y->sz + 2) {
/* Barrett reduction for moderate-sized moduli (>= 4 limbs where setup is worthwhile).
* Barrett's precondition is x < 2^(2*bits(m)); inputs beyond ~2*m.sz limbs
* violate it and the algorithm silently truncates high limbs. Fall through
* to general division for those. */
if (y->sz >= 4 && y->sz <= 16 && x->sz >= y->sz + 2 && x->sz <= 2 * y->sz) {
mpz_t mu;
mpz_init_temp(ctx, &mu, y->sz + 1);
mpz_barrett_mu(ctx, &mu, y);
@@ -4030,7 +4049,7 @@ mpz_get_str(mpz_ctx_t *ctx, char *s, mrb_int sz, mrb_int base, mpz_t *x)
}
// convert to character
for (mp_limb b=b2; b>=base; b/=(mp_limb)base) {
for (mp_limb b=b2; b>=(mp_limb)base; b/=(mp_limb)base) {
char a0 = (char)(a % base);
if (a0 < 10) a0 += '0';
else a0 += 'a' - 10;
@@ -4817,7 +4836,11 @@ mpz_power_of_2_p(mpz_t *x)
return (limb != 0) && ((limb & (limb - 1)) == 0);
}
/* Binary GCD algorithm (Stein's algorithm) - faster than Euclidean GCD */
/* Binary GCD (Stein's algorithm): factor out common powers of 2,
then iterate on odd operands with subtract + trailing-zero shift.
For heavily unbalanced pairs (one operand has at least two more
limbs than the other) a single Euclidean step via mpz_mod replaces
many Stein subtracts. */
static void
mpz_gcd(mpz_ctx_t *ctx, mpz_t *gg, mpz_t *aa, mpz_t *bb)
{
@@ -4889,14 +4912,29 @@ mpz_gcd(mpz_ctx_t *ctx, mpz_t *gg, mpz_t *aa, mpz_t *bb)
mpz_div_2exp(ctx, &a, &a, a_zeros);
mpz_div_2exp(ctx, &b, &b, b_zeros);
/* Euclidean algorithm for multi-limb numbers */
/* Stein main loop. Invariant: a and b are positive and odd.
Euclidean fallback when b has >=2 more limbs than a. */
while (!zero_p(&b)) {
mpz_t temp;
mpz_init_temp(ctx, &temp, a.sz);
mpz_mod(ctx, &temp, &a, &b);
mpz_move(ctx, &a, &b);
mpz_move(ctx, &b, &temp);
mpz_clear(ctx, &temp);
if (mpz_cmp(ctx, &a, &b) > 0) {
mpz_swap(&a, &b);
}
if (b.sz >= a.sz + 2) {
mpz_t temp;
mpz_init_temp(ctx, &temp, a.sz);
mpz_mod(ctx, &temp, &b, &a);
mpz_move(ctx, &b, &temp);
mpz_clear(ctx, &temp);
if (zero_p(&b)) break;
size_t bz = mpz_trailing_zeros(&b);
if (bz > 0)
mpz_div_2exp(ctx, &b, &b, bz);
}
else {
mpz_sub(ctx, &b, &b, &a);
if (zero_p(&b)) break;
size_t bz = mpz_trailing_zeros(&b);
mpz_div_2exp(ctx, &b, &b, bz);
}
}
mpz_mul_2exp(ctx, gg, &a, shift);
mpz_clear(ctx, &a);
@@ -5215,12 +5253,19 @@ mpz_powm_montgomery(mpz_ctx_t *ctx, mpz_t *result,
mpz_init(ctx, &one_mont);
mpz_montgomery_reduce(ctx, &one_mont, &R2, n, rho);
/* Convert base to Montgomery form: base_mont = base * R mod n = REDC(base * R^2) */
mpz_t base_mont, temp;
/* Convert base to Montgomery form: base_mont = base * R mod n = REDC(base * R^2).
* REDC requires its input T to satisfy T < R*N. If `base` is not already
* reduced (e.g. base >= n), `base * R^2` can exceed R*N and REDC produces
* a wrong result. Pre-reduce base modulo n via mpz_mmod (the general
* division path) -- both operands are non-negative here so this is
* semantically equivalent to mpz_mod. */
mpz_t base_mont, base_reduced, temp;
mpz_init(ctx, &base_mont);
mpz_init(ctx, &base_reduced);
mpz_init_temp(ctx, &temp, n->sz * 4);
mpz_mul(ctx, &temp, (mpz_t*)base, &R2);
mpz_mmod(ctx, &base_reduced, (mpz_t*)base, (mpz_t*)n);
mpz_mul(ctx, &temp, &base_reduced, &R2);
mpz_montgomery_reduce(ctx, &base_mont, &temp, n, rho);
/* Initialize accumulator to 1 in Montgomery form */
@@ -5252,6 +5297,7 @@ mpz_powm_montgomery(mpz_ctx_t *ctx, mpz_t *result,
mpz_clear(ctx, &R2);
mpz_clear(ctx, &one_mont);
mpz_clear(ctx, &base_mont);
mpz_clear(ctx, &base_reduced);
mpz_clear(ctx, &temp);
mpz_clear(ctx, &acc);
pool_restore(ctx, pool_state);
+2
View File
@@ -4,6 +4,8 @@ MRuby::Gem::Specification.new('mruby-bigint') do |spec|
spec.summary = 'Integer class extension to multiple-precision'
spec.build.defines << "MRB_USE_BIGINT"
spec.add_test_dependency('mruby-numeric-ext', :core => 'mruby-numeric-ext')
spec.build.libmruby_core_objs << Dir.glob(File.join(__dir__, "core/**/*.c")).map { |fn|
objfile(fn.relative_path_from(__dir__).pathmap("#{spec.build_dir}/%X"))
}
+67
View File
@@ -150,8 +150,75 @@ assert 'Bigint pow' do
# assert_equal(-1041439304, n.pow(n, -1234567890))
end
assert 'Bigint Integer#pow(e, m) - Montgomery path' do
# Regression: mpz_powm_montgomery() failed to pre-reduce base mod n,
# producing wrong results when base >= n. Also trim() must restore
# the canonical sn=0 when sz becomes 0, otherwise an inconsistent
# zero bignum (sn!=0, sz=0) propagates through the squaring loop.
m = (2**40) + 1
assert_equal 1, (2**160).pow(2, m)
assert_equal 1, (2**320).pow(2, m)
assert_equal 8, ((2**160) + 1).pow(3, m)
m2 = (2**100) + 3
assert_equal (3**500) % m2, (3**500).pow(1, m2)
assert_equal ((5**300) ** 7) % m2, (5**300).pow(7, m2)
end
assert 'Bigint Integer#remainder large operand' do
# Regression: mpz_mod's Barrett path didn't enforce its precondition
# x < 2^(2*bits(m)), so it silently truncated high limbs when x was
# much larger than m^2, producing the wrong remainder. Integer#%
# took the udiv path and worked, but Integer#remainder went through
# mpz_mod and was broken.
m = (2**100) + 3
assert_equal (3**500) % m, (3**500).remainder(m)
assert_equal (5**500) % ((2**150) + 1), (5**500).remainder((2**150) + 1)
assert_equal (2**400) % ((2**130) + 1), (2**400).remainder((2**130) + 1)
end
assert 'Bigint abs' do
n = 1<<65
assert_equal 36893488147419103232, n.abs
assert_equal 36893488147419103232, (-n).abs
end
assert 'Bigint gcd' do
# zero cases
assert_equal 0, 0.gcd(0)
n = 1 << 200
assert_equal n, n.gcd(0)
assert_equal n, 0.gcd(n)
# power-of-2 fast path
assert_equal 1 << 100, (1 << 200).gcd(1 << 100)
assert_equal 1 << 100, (1 << 100).gcd(1 << 200)
assert_equal 1 << 40, (10 ** 50).gcd(1 << 40)
# negative operands: result is the positive GCD
a = 1 << 200
b = 3 << 200
assert_equal a, a.gcd(b)
assert_equal a, (-a).gcd(b)
assert_equal a, a.gcd(-b)
assert_equal a, (-a).gcd(-b)
# balanced multi-limb with known common factor
fib1000 = (1..1000).inject([0, 1]) { |(x, y), _| [y, x + y] }[0]
common = fib1000
k, m = 1_000_003, 1_000_033 # small coprime primes
assert_equal common, (common * k).gcd(common * m)
assert_equal common, (common * m).gcd(common * k)
# unbalanced: small coprime vs large
big = common * k
assert_equal 1, big.gcd(m)
assert_equal 1, m.gcd(big)
# Fibonacci neighbors are always coprime
f100 = (1..100).inject([0, 1]) { |(x, y), _| [y, x + y] }[0]
f101 = (1..101).inject([0, 1]) { |(x, y), _| [y, x + y] }[0]
assert_equal 1, f100.gcd(f101)
# Euclidean fallback path: operand sizes differ by several limbs
assert_equal 7, (7 * (1 << 4000)).gcd(7 * 13)
end
+1 -1
View File
@@ -28,7 +28,7 @@ MRuby::Gem::Specification.new('mruby-bin-config') do |spec|
if iscross
build.products << mruby_config_path
else
build.bins << mruby_config
build.products << build.define_installer(mruby_config_path)
end
directory mruby_config_dir

Some files were not shown because too many files have changed in this diff Show More