From 58ac19c3d4f1e8649b7208d57cffccb381ceec09 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Fri, 19 Jun 2026 11:51:52 +0200 Subject: [PATCH 1/2] ResumableParser: use throw rather than raise for handled EOS Since the exception will be swallowed, building a message and backtrace is just a waste of time. --- ext/json/ext/parser/parser.c | 39 ++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/ext/json/ext/parser/parser.c b/ext/json/ext/parser/parser.c index a39b6220..2c5c4a5a 100644 --- a/ext/json/ext/parser/parser.c +++ b/ext/json/ext/parser/parser.c @@ -677,10 +677,17 @@ static VALUE parse_error_new(JSON_ParserState *state, VALUE message, long line, NORETURN(static) void raise_parse_error(const char *format, JSON_ParserState *state, bool eos) { - VALUE message = build_parse_error_message(format, state); - if (state->parser) { // line and columns can't be accurate in resumable - rb_exc_raise(parse_error_new(state, message, 0, 0, eos)); + if (state->parser) { + if (eos) { + // the error will be swallowed by ResumableParser#parse, so no + // point building a message or backtrace. + rb_throw_obj(state->parser, state->parser); + } else { + // line and columns can't be accurate in resumable + rb_exc_raise(parse_error_new(state, build_parse_error_message(format, state), 0, 0, eos)); + } } else { + VALUE message = build_parse_error_message(format, state); long line, column; cursor_position(state, &line, &column); rb_str_catf(message, " at line %ld column %ld", line, column); @@ -2379,14 +2386,25 @@ static VALUE cResumableParser_feed(VALUE self, VALUE str) struct json_parse_any_args { JSON_ParserState *state; JSON_ParserConfig *config; + VALUE parser; }; -static VALUE json_parse_any_resumable_safe(VALUE _args) +static VALUE json_parse_any_resumable_safe0(RB_BLOCK_CALL_FUNC_ARGLIST(yielded_arg, _args)) { struct json_parse_any_args *args = (struct json_parse_any_args *)_args; return (VALUE)json_parse_any(args->state, args->config, true); } +static VALUE json_parse_any_resumable_safe(VALUE _args) +{ + struct json_parse_any_args *args = (struct json_parse_any_args *)_args; + VALUE result = rb_catch_obj(args->parser, json_parse_any_resumable_safe0, _args); + if (result == args->parser) { + return (VALUE)false; + } + return result; +} + static JSON_ResumableParser *ResumableParser_acquire(VALUE self, bool lock) { JSON_ResumableParser *parser = cResumableParser_get(self); @@ -2455,25 +2473,20 @@ static VALUE cResumableParser_parse(VALUE self) struct json_parse_any_args args = { .state = &parser->state, .config = &parser->config, + .parser = self, }; int status; const char *initial_cursor = parser->state.cursor; parser->complete = rb_protect(json_parse_any_resumable_safe, (VALUE)&args, &status); + if (status) { - VALUE error_source = rb_ivar_get(rb_errinfo(), i_at_eos); - if (error_source == self) { - parser->complete = false; // is an EOS error raised by ourself - rb_set_errinfo(Qnil); - status = 0; - } else { - parser->complete = true; // a parse error is considered complete - } + parser->complete = true; // a parse error is considered complete } parser->parsed_bytes += parser->state.cursor - initial_cursor; parser->incomplete_bytes = parser->complete ? 0 : parser->state.end - parser->state.cursor; - parser->in_use = false; + if (status) { rb_jump_tag(status); // reraise } From 3a687496b058d7a0de3d2cbd1cb2a4f1ace851e1 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Fri, 19 Jun 2026 12:46:23 +0200 Subject: [PATCH 2/2] Workaround TruffleRuby buggy `rb_catch_obj` implementation Somehow on TruffleRuby `rb_catch_obj` straight out doesn't call the passed function, acting as a noop. --- ext/json/ext/parser/extconf.rb | 2 ++ ext/json/ext/parser/parser.c | 47 +++++++++++++++++++++++++--------- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/ext/json/ext/parser/extconf.rb b/ext/json/ext/parser/extconf.rb index bd537c9c..8c5bdb66 100644 --- a/ext/json/ext/parser/extconf.rb +++ b/ext/json/ext/parser/extconf.rb @@ -2,6 +2,8 @@ require 'mkmf' $defs << "-DJSON_DEBUG" if ENV.fetch("JSON_DEBUG", "0") != "0" +$defs << "-DJSON_WORKAROUND_RB_CATCH_BUG" if RUBY_ENGINE == 'truffleruby' + have_func("rb_enc_interned_str", "ruby/encoding.h") # RUBY_VERSION >= 3.0 have_func("rb_str_to_interned_str", "ruby.h") # RUBY_VERSION >= 3.0 have_func("rb_hash_new_capa", "ruby.h") # RUBY_VERSION >= 3.2 diff --git a/ext/json/ext/parser/parser.c b/ext/json/ext/parser/parser.c index 2c5c4a5a..057367bc 100644 --- a/ext/json/ext/parser/parser.c +++ b/ext/json/ext/parser/parser.c @@ -5,7 +5,7 @@ static VALUE mJSON, eNestingError, eParserError, Encoding_UTF_8; static VALUE CNaN, CInfinity, CMinusInfinity; -static ID i_new, i_try_convert, i_uminus, i_encode, i_at_line, i_at_column, i_at_eos; +static ID i_new, i_try_convert, i_uminus, i_encode, i_at_line, i_at_column; static VALUE sym_max_nesting, sym_allow_nan, sym_allow_trailing_comma, sym_allow_comments, sym_allow_control_characters, sym_allow_invalid_escape, sym_symbolize_names, @@ -669,19 +669,46 @@ static VALUE parse_error_new(JSON_ParserState *state, VALUE message, long line, VALUE exc = rb_exc_new_str(eParserError, message); rb_ivar_set(exc, i_at_line, LONG2NUM(line)); rb_ivar_set(exc, i_at_column, LONG2NUM(column)); - if (eos && state->parser) { - rb_ivar_set(exc, i_at_eos, state->parser); - } return exc; } +#ifdef JSON_WORKAROUND_RB_CATCH_BUG +#define JSON_CATCH_FUNC_ARGLIST(yielded_arg, func_args) VALUE func_args + +NORETURN(static) void parser_throw_eos(VALUE parser) +{ + VALUE exc = rb_exc_new_str(eParserError, rb_utf8_str_new_cstr("EOS")); + rb_ivar_set(exc, rb_intern("@resumable_parser_eos"), parser); + rb_exc_raise(exc); +} + +static VALUE parser_catch_eos(VALUE parser, VALUE (*func)(VALUE args), VALUE func_args) +{ + int status; + VALUE result = rb_protect(func, func_args, &status); + if (status) { + VALUE error_source = rb_ivar_get(rb_errinfo(), rb_intern("@resumable_parser_eos")); + if (error_source == parser) { + rb_set_errinfo(Qnil); + return parser; + } + rb_jump_tag(status); + } + return result; +} +#else +#define JSON_CATCH_FUNC_ARGLIST RB_BLOCK_CALL_FUNC_ARGLIST +#define parser_throw_eos(parser) rb_throw_obj(parser, parser) +#define parser_catch_eos(parser, func, func_args) rb_catch_obj(parser, func, func_args) +#endif + NORETURN(static) void raise_parse_error(const char *format, JSON_ParserState *state, bool eos) { if (state->parser) { if (eos) { // the error will be swallowed by ResumableParser#parse, so no // point building a message or backtrace. - rb_throw_obj(state->parser, state->parser); + parser_throw_eos(state->parser); } else { // line and columns can't be accurate in resumable rb_exc_raise(parse_error_new(state, build_parse_error_message(format, state), 0, 0, eos)); @@ -2389,7 +2416,7 @@ struct json_parse_any_args { VALUE parser; }; -static VALUE json_parse_any_resumable_safe0(RB_BLOCK_CALL_FUNC_ARGLIST(yielded_arg, _args)) +static VALUE json_parse_any_resumable_safe0(JSON_CATCH_FUNC_ARGLIST(yielded_arg, _args)) { struct json_parse_any_args *args = (struct json_parse_any_args *)_args; return (VALUE)json_parse_any(args->state, args->config, true); @@ -2398,11 +2425,8 @@ static VALUE json_parse_any_resumable_safe0(RB_BLOCK_CALL_FUNC_ARGLIST(yielded_a static VALUE json_parse_any_resumable_safe(VALUE _args) { struct json_parse_any_args *args = (struct json_parse_any_args *)_args; - VALUE result = rb_catch_obj(args->parser, json_parse_any_resumable_safe0, _args); - if (result == args->parser) { - return (VALUE)false; - } - return result; + VALUE result = parser_catch_eos(args->parser, json_parse_any_resumable_safe0, _args); + return result == args->parser ? Qfalse : result; } static JSON_ResumableParser *ResumableParser_acquire(VALUE self, bool lock) @@ -2778,7 +2802,6 @@ void Init_parser(void) i_encode = rb_intern("encode"); i_at_line = rb_intern("@line"); i_at_column = rb_intern("@column"); - i_at_eos = rb_intern("@eos"); binary_encindex = rb_ascii8bit_encindex(); utf8_encindex = rb_utf8_encindex();