diff --git a/.commit/config.yml b/.commit/config.yml index cafdc74..b870728 100644 --- a/.commit/config.yml +++ b/.commit/config.yml @@ -53,4 +53,6 @@ ruby: spec.files = Dir["CHANGELOG.md", "README.md", "LICENSE", "lib/**/*"] spec.require_path = "lib" - spec.extensions = %w[ext/llhttp/extconf.rb] + spec.extensions = %w[ext/Rakefile] + + spec.add_dependency "ffi-compiler", "~> 1.0" diff --git a/LICENSE b/LICENSE index c7ad8c2..1ee96d4 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ This software is licensed under the MIT License. -Copyright 2020 Metabahn. +Copyright 2020-2021 Metabahn. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/Rakefile b/Rakefile index 264a5cb..2aa9a65 100644 --- a/Rakefile +++ b/Rakefile @@ -1,11 +1,9 @@ # frozen_string_literal: true require "fileutils" -require "rake/extensiontask" -Rake::ExtensionTask.new "llhttp_ext" do |ext| - ext.ext_dir = "ext/llhttp" - ext.lib_dir = "lib/llhttp" +task :compile do + system "cd ext && bundle exec rake" end task test: :compile do diff --git a/benchmarks/ffi/Gemfile b/benchmarks/ffi/Gemfile new file mode 100644 index 0000000..bf76ed8 --- /dev/null +++ b/benchmarks/ffi/Gemfile @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +gem "llhttp", path: "../.." + +gem "benchmark-ips" +gem "ruby-prof" diff --git a/benchmarks/ffi/run.rb b/benchmarks/ffi/run.rb new file mode 100644 index 0000000..9bc3572 --- /dev/null +++ b/benchmarks/ffi/run.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require_relative "../shared" + +benchmark + +# require "ruby-prof" + +# delegate = LLHttp::Delegate.new +# instance = LLHttp::Parser.new(delegate, type: :request) + +# result = RubyProf.profile { +# 1_000_000.times do +# parse(instance) +# instance.finish +# end +# } + +# printer = RubyProf::GraphPrinter.new(result) +# printer.print(STDOUT, {}) diff --git a/benchmarks/mri/Gemfile b/benchmarks/mri/Gemfile new file mode 100644 index 0000000..7b56a3f --- /dev/null +++ b/benchmarks/mri/Gemfile @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +gem "benchmark-ips" +gem "llhttp" diff --git a/benchmarks/mri/run.rb b/benchmarks/mri/run.rb new file mode 100644 index 0000000..1b0314c --- /dev/null +++ b/benchmarks/mri/run.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +require_relative "../shared" + +benchmark diff --git a/benchmarks/shared.rb b/benchmarks/shared.rb new file mode 100644 index 0000000..1b42d0f --- /dev/null +++ b/benchmarks/shared.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require "benchmark/ips" +require "llhttp" + +def parse(instance) + instance << "GET / HTTP/1.1\r\n" + instance << "content-length: 18\r\n" + instance << "\r\n" + instance << "body1\n" + instance << "body2\n" + instance << "body3\n" + instance << "\r\n" +end + +def benchmark + delegate = LLHttp::Delegate.new + instance = LLHttp::Parser.new(delegate, type: :request) + + Benchmark.ips do |x| + x.report do + parse(instance) + instance.finish + end + end +end diff --git a/ext/Rakefile b/ext/Rakefile new file mode 100644 index 0000000..e169dcd --- /dev/null +++ b/ext/Rakefile @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +require "ffi-compiler/compile_task" + +FFI::Compiler::CompileTask.new("llhttp-ext") diff --git a/ext/llhttp/extconf.rb b/ext/llhttp/extconf.rb deleted file mode 100644 index 65cfc3f..0000000 --- a/ext/llhttp/extconf.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -require "mkmf" - -dir_config "llhttp_ext" - -create_makefile "llhttp_ext" diff --git a/ext/llhttp/llhttp_ext.c b/ext/llhttp/llhttp_ext.c index c31ea54..15ab37e 100644 --- a/ext/llhttp/llhttp_ext.c +++ b/ext/llhttp/llhttp_ext.c @@ -21,223 +21,147 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. -#include - +#include +#include #include "llhttp.h" -static VALUE mLLHttp, cParser, eError; +typedef struct rb_llhttp_callbacks_s rb_llhttp_callbacks_t; -static void rb_llhttp_free(llhttp_t *parser) { - if (parser) { - free(parser->settings); - free(parser); - } -} +typedef int (*rb_llhttp_data_cb)(const char *data, size_t length); +typedef int (*rb_llhttp_cb)(); -VALUE rb_llhttp_allocate(VALUE klass) { - llhttp_t *parser = (llhttp_t *)malloc(sizeof(llhttp_t)); - llhttp_settings_t *settings = (llhttp_settings_t *)malloc(sizeof(llhttp_settings_t)); +struct rb_llhttp_callbacks_s { + /* Possible return values 0, -1, `HPE_PAUSED` */ + rb_llhttp_cb on_message_begin; - llhttp_settings_init(settings); - llhttp_init(parser, HTTP_BOTH, settings); + rb_llhttp_data_cb on_url; + rb_llhttp_data_cb on_status; + rb_llhttp_data_cb on_header_field; + rb_llhttp_data_cb on_header_value; - return Data_Wrap_Struct(klass, 0, rb_llhttp_free, parser); -} - -void rb_llhttp_callback_call(VALUE delegate, const char *name) { - rb_funcall(delegate, rb_intern(name), 0); -} + /* Possible return values: + * 0 - Proceed normally + * 1 - Assume that request/response has no body, and proceed to parsing the + * next message + * 2 - Assume absence of body (as above) and make `llhttp_execute()` return + * `HPE_PAUSED_UPGRADE` + * -1 - Error + * `HPE_PAUSED` + */ + rb_llhttp_cb on_headers_complete; -void rb_llhttp_data_callback_call(VALUE delegate, const char *name, char *data, size_t length) { - rb_funcall(delegate, rb_intern(name), 1, rb_str_new(data, length)); -} + rb_llhttp_data_cb on_body; -int rb_llhttp_on_message_begin(llhttp_t *parser) { - VALUE delegate = (VALUE)parser->data; + /* Possible return values 0, -1, `HPE_PAUSED` */ + rb_llhttp_cb on_message_complete; - rb_llhttp_callback_call(delegate, "on_message_begin"); + /* When on_chunk_header is called, the current chunk length is stored + * in parser->content_length. + * Possible return values 0, -1, `HPE_PAUSED` + */ + rb_llhttp_cb on_chunk_header; + rb_llhttp_cb on_chunk_complete; +}; +int rb_llhttp_on_message_begin(llhttp_t *parser) { + rb_llhttp_callbacks_t* callbacks = parser->data; + callbacks->on_message_begin(); return 0; } int rb_llhttp_on_url(llhttp_t *parser, char *data, size_t length) { - VALUE delegate = (VALUE)parser->data; - - rb_llhttp_data_callback_call(delegate, "on_url", data, length); - + rb_llhttp_callbacks_t* callbacks = parser->data; + callbacks->on_url(data, length); return 0; } int rb_llhttp_on_status(llhttp_t *parser, char *data, size_t length) { - VALUE delegate = (VALUE)parser->data; - - rb_llhttp_data_callback_call(delegate, "on_status", data, length); - + rb_llhttp_callbacks_t* callbacks = parser->data; + callbacks->on_status(data, length); return 0; } int rb_llhttp_on_header_field(llhttp_t *parser, char *data, size_t length) { - VALUE delegate = (VALUE)parser->data; - - rb_llhttp_data_callback_call(delegate, "on_header_field", data, length); - + rb_llhttp_callbacks_t* callbacks = parser->data; + callbacks->on_header_field(data, length); return 0; } int rb_llhttp_on_header_value(llhttp_t *parser, char *data, size_t length) { - VALUE delegate = (VALUE)parser->data; - - rb_llhttp_data_callback_call(delegate, "on_header_value", data, length); - + rb_llhttp_callbacks_t* callbacks = parser->data; + callbacks->on_header_value(data, length); return 0; } int rb_llhttp_on_headers_complete(llhttp_t *parser) { - VALUE delegate = (VALUE)parser->data; - - rb_llhttp_callback_call(delegate, "on_headers_complete"); - + rb_llhttp_callbacks_t* callbacks = parser->data; + callbacks->on_headers_complete(); return 0; } int rb_llhttp_on_body(llhttp_t *parser, char *data, size_t length) { - VALUE delegate = (VALUE)parser->data; - - rb_llhttp_data_callback_call(delegate, "on_body", data, length); - + rb_llhttp_callbacks_t* callbacks = parser->data; + callbacks->on_body(data, length); return 0; } int rb_llhttp_on_message_complete(llhttp_t *parser) { - VALUE delegate = (VALUE)parser->data; - - rb_llhttp_callback_call(delegate, "on_message_complete"); - + rb_llhttp_callbacks_t* callbacks = parser->data; + callbacks->on_message_complete(); return 0; } int rb_llhttp_on_chunk_header(llhttp_t *parser) { - VALUE delegate = (VALUE)parser->data; - - rb_llhttp_callback_call(delegate, "on_chunk_header"); - + rb_llhttp_callbacks_t* callbacks = parser->data; + callbacks->on_chunk_header(); return 0; } int rb_llhttp_on_chunk_complete(llhttp_t *parser) { - VALUE delegate = (VALUE)parser->data; - - rb_llhttp_callback_call(delegate, "on_chunk_complete"); - + rb_llhttp_callbacks_t* callbacks = parser->data; + callbacks->on_chunk_complete(); return 0; } -VALUE rb_llhttp_parse(VALUE self, VALUE data) { - llhttp_t *parser; - - Data_Get_Struct(self, llhttp_t, parser); - - enum llhttp_errno err = llhttp_execute(parser, RSTRING_PTR(data), RSTRING_LEN(data)); - - if (err != HPE_OK) { - rb_raise(eError, "Error Parsing data: %s %s", llhttp_errno_name(err), parser->reason); - } - - return Qtrue; -} - -VALUE rb_llhttp_finish(VALUE self) { - llhttp_t *parser; - - Data_Get_Struct(self, llhttp_t, parser); +// TODO: Don't forget the finalizer to dealloc things. - enum llhttp_errno err = llhttp_finish(parser); - - if (err != HPE_OK) { - rb_raise(eError, "Error Parsing data: %s %s", llhttp_errno_name(err), parser->reason); - } - - return Qtrue; -} - -VALUE rb_llhttp_content_length(VALUE self) { - llhttp_t *parser; - - Data_Get_Struct(self, llhttp_t, parser); - - return UINT2NUM(parser->content_length); -} - -VALUE rb_llhttp_method(VALUE self) { - llhttp_t *parser; - - Data_Get_Struct(self, llhttp_t, parser); - - return rb_str_new_cstr(llhttp_method_name(parser->method)); -} - -VALUE rb_llhttp_status_code(VALUE self) { - llhttp_t *parser; - - Data_Get_Struct(self, llhttp_t, parser); - - return UINT2NUM(parser->status_code); -} - -VALUE rb_llhttp_keep_alive(VALUE self) { - llhttp_t *parser; - - Data_Get_Struct(self, llhttp_t, parser); - - int ret = llhttp_should_keep_alive(parser); - - return ret == 1 ? Qtrue : Qfalse; -} - -static VALUE rb_llhttp_init(VALUE self, VALUE type) { - llhttp_t *parser; - - Data_Get_Struct(self, llhttp_t, parser); +llhttp_t* rb_llhttp_init(int type, rb_llhttp_callbacks_t* callbacks) { + llhttp_t *parser = (llhttp_t *)malloc(sizeof(llhttp_t)); + llhttp_settings_t *settings = (llhttp_settings_t *)malloc(sizeof(llhttp_settings_t)); - llhttp_settings_t *settings = parser->settings; + llhttp_settings_init(settings); settings->on_message_begin = (llhttp_cb)rb_llhttp_on_message_begin; + settings->on_url = (llhttp_data_cb)rb_llhttp_on_url; settings->on_status = (llhttp_data_cb)rb_llhttp_on_status; settings->on_header_field = (llhttp_data_cb)rb_llhttp_on_header_field; settings->on_header_value = (llhttp_data_cb)rb_llhttp_on_header_value; + settings->on_headers_complete = (llhttp_cb)rb_llhttp_on_headers_complete; + settings->on_body = (llhttp_data_cb)rb_llhttp_on_body; + settings->on_message_complete = (llhttp_cb)rb_llhttp_on_message_complete; + settings->on_chunk_header = (llhttp_cb)rb_llhttp_on_chunk_header; settings->on_chunk_complete = (llhttp_cb)rb_llhttp_on_chunk_complete; - llhttp_init(parser, FIX2INT(type), settings); + llhttp_init(parser, type, settings); - // Store a pointer to the delegate for lookup in callbacks. - // - VALUE delegate = rb_iv_get(self, "@delegate"); - parser->data = (void*)delegate; + parser->data = callbacks; - return Qtrue; + return parser; } -void Init_llhttp_ext(void) { - mLLHttp = rb_const_get(rb_cObject, rb_intern("LLHttp")); - cParser = rb_const_get(mLLHttp, rb_intern("Parser")); - eError = rb_const_get(mLLHttp, rb_intern("Error")); - - rb_define_alloc_func(cParser, rb_llhttp_allocate); - - rb_define_method(cParser, "<<", rb_llhttp_parse, 1); - rb_define_method(cParser, "parse", rb_llhttp_parse, 1); - rb_define_method(cParser, "finish", rb_llhttp_finish, 0); - - rb_define_method(cParser, "content_length", rb_llhttp_content_length, 0); - rb_define_method(cParser, "method", rb_llhttp_method, 0); - rb_define_method(cParser, "status_code", rb_llhttp_status_code, 0); +uint64_t rb_llhttp_content_length(llhttp_t* parser) { + return parser->content_length; +} - rb_define_method(cParser, "keep_alive?", rb_llhttp_keep_alive, 0); +const char* rb_llhttp_method_name(llhttp_t* parser) { + return llhttp_method_name(parser->method); +} - rb_define_private_method(cParser, "llhttp_init", rb_llhttp_init, 1); +uint16_t rb_llhttp_status_code(llhttp_t* parser) { + return parser->status_code; } diff --git a/lib/llhttp.rb b/lib/llhttp.rb index 7dd7933..58b5267 100644 --- a/lib/llhttp.rb +++ b/lib/llhttp.rb @@ -1,8 +1,43 @@ # frozen_string_literal: true +require "ffi" +require "ffi-compiler/loader" + module LLHttp require_relative "llhttp/delegate" require_relative "llhttp/error" require_relative "llhttp/parser" require_relative "llhttp/version" + + extend FFI::Library + ffi_lib(FFI::Compiler::Loader.find("llhttp-ext")) + + # TODO: These should return int, which should be returned to llhttp. Document possible return values. + # + callback :llhttp_data_cb, [:pointer, :size_t], :void + callback :llhttp_cb, [], :void + + class Callbacks < FFI::Struct + layout :on_message_begin, :llhttp_cb, + :on_url, :llhttp_data_cb, + :on_status, :llhttp_data_cb, + :on_header_field, :llhttp_data_cb, + :on_header_value, :llhttp_data_cb, + :on_headers_complete, :llhttp_cb, + :on_body, :llhttp_data_cb, + :on_message_complete, :llhttp_cb, + :on_chunk_header, :llhttp_cb, + :on_chunk_complete, :llhttp_cb + end + + attach_function :rb_llhttp_init, [:int, Callbacks.by_ref], :pointer + attach_function :rb_llhttp_content_length, [:pointer], :uint64 + attach_function :rb_llhttp_method_name, [:pointer], :string + attach_function :rb_llhttp_status_code, [:pointer], :uint16 + + attach_function :llhttp_execute, [:pointer, :pointer, :size_t], :int + # attach_function :llhttp_errno_name, [:int], :pointer + # attach_function :llhttp_get_error_reason, [Instance.by_ref], :pointer + attach_function :llhttp_should_keep_alive, [:pointer], :int + attach_function :llhttp_finish, [:pointer], :int end diff --git a/lib/llhttp/parser.rb b/lib/llhttp/parser.rb index 6f02644..ab43381 100644 --- a/lib/llhttp/parser.rb +++ b/lib/llhttp/parser.rb @@ -20,7 +20,7 @@ module LLHttp # Introspection # # * `LLHttp::Parser#content_length` returns the content length of the current request. - # * `LLHttp::Parser#method` returns the method of the current response. + # * `LLHttp::Parser#method_name` returns the method of the current response. # * `LLHttp::Parser#status_code` returns the status code of the current response. # * `LLHttp::Parser#keep_alive?` returns `true` if there might be more messages. # @@ -31,16 +31,78 @@ module LLHttp class Parser LLHTTP_TYPES = {both: 0, request: 1, response: 2}.freeze - # [public] The parser type; one of: `both`, `request`, or `response`. + # [public] The parser type; one of: `:both`, `:request`, or `:response`. # attr_reader :type def initialize(delegate, type: :both) @type, @delegate = type.to_sym, delegate - llhttp_init(LLHTTP_TYPES.fetch(@type)) + @callbacks = Callbacks.new + @callbacks[:on_message_begin] = delegate.method(:on_message_begin).to_proc + @callbacks[:on_url] = method(:on_url).to_proc + @callbacks[:on_status] = method(:on_status).to_proc + @callbacks[:on_header_field] = method(:on_header_field).to_proc + @callbacks[:on_header_value] = method(:on_header_value).to_proc + @callbacks[:on_headers_complete] = delegate.method(:on_headers_complete).to_proc + @callbacks[:on_body] = method(:on_body).to_proc + @callbacks[:on_message_complete] = delegate.method(:on_message_complete).to_proc + @callbacks[:on_chunk_header] = delegate.method(:on_chunk_header).to_proc + @callbacks[:on_chunk_complete] = delegate.method(:on_chunk_complete).to_proc + + @pointer = LLHttp.rb_llhttp_init(LLHTTP_TYPES.fetch(@type), @callbacks) + end + + def parse(data) + errno = LLHttp.llhttp_execute(@pointer, data, data.length) + raise build_error(errno) if errno > 0 + end + alias_method :<<, :parse + + def content_length + LLHttp.rb_llhttp_content_length(@pointer) + end + + def method_name + LLHttp.rb_llhttp_method_name(@pointer) + end + + def status_code + LLHttp.rb_llhttp_status_code(@pointer) + end + + def keep_alive? + LLHttp.llhttp_should_keep_alive(@pointer) == 1 + end + + def finish + LLHttp.llhttp_finish(@pointer) + end + + private def on_url(buffer, length) + @delegate.on_url(buffer.get_bytes(0, length)) + end + + private def on_status(buffer, length) + @delegate.on_status(buffer.get_bytes(0, length)) + end + + private def on_header_field(buffer, length) + @delegate.on_header_field(buffer.get_bytes(0, length)) + end + + private def on_header_value(buffer, length) + @delegate.on_header_value(buffer.get_bytes(0, length)) + end + + private def on_body(buffer, length) + @delegate.on_body(buffer.get_bytes(0, length)) + end + + private def build_error(errno) + # Error.new("Error Parsing data: #{LLHttp.llhttp_errno_name(errno).read_string} #{LLHttp.llhttp_get_error_reason(self).read_string}") + + Error.new("TODO") end end end - -require_relative "llhttp_ext" diff --git a/llhttp.gemspec b/llhttp.gemspec index fdc2952..9e01134 100644 --- a/llhttp.gemspec +++ b/llhttp.gemspec @@ -19,5 +19,7 @@ Gem::Specification.new do |spec| spec.files = Dir["CHANGELOG.md", "README.md", "LICENSE", "lib/**/*"] spec.require_path = "lib" - spec.extensions = %w[ext/llhttp/extconf.rb] + spec.extensions = %w[ext/Rakefile] + + spec.add_dependency "ffi-compiler", "~> 1.0" end diff --git a/spec/integration/parser/request_parsing/errors_spec.rb b/spec/integration/parser/request_parsing/errors_spec.rb index 5c3f7ee..4abca0e 100644 --- a/spec/integration/parser/request_parsing/errors_spec.rb +++ b/spec/integration/parser/request_parsing/errors_spec.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "llhttp" + RSpec.describe "errors when parsing requests" do let(:instance) { LLHttp::Parser.new(LLHttp::Delegate.new, type: :request) diff --git a/spec/integration/parser/request_parsing/introspection_spec.rb b/spec/integration/parser/request_parsing/introspection_spec.rb index 5ac90e3..29cd4dc 100644 --- a/spec/integration/parser/request_parsing/introspection_spec.rb +++ b/spec/integration/parser/request_parsing/introspection_spec.rb @@ -51,11 +51,11 @@ def on_headers_complete end end - describe "method" do + describe "method_name" do let(:extension) { proc { def on_headers_complete - @context.instance_variable_set(:@method, @context.instance.method) + @context.instance_variable_set(:@method_name, @context.instance.method_name) end } } @@ -63,7 +63,23 @@ def on_headers_complete it "returns the content" do parse - expect(@method).to eq("GET") + expect(@method_name).to eq("GET") + end + end + + describe "keep_alive" do + let(:extension) { + proc { + def on_headers_complete + @context.instance_variable_set(:@keep_alive, @context.instance.keep_alive?) + end + } + } + + it "returns the keep alive state" do + parse + + expect(@keep_alive).to eq(true) end end end diff --git a/spec/integration/parser/response_parsing/introspection_spec.rb b/spec/integration/parser/response_parsing/introspection_spec.rb index 0a57505..f8809ca 100644 --- a/spec/integration/parser/response_parsing/introspection_spec.rb +++ b/spec/integration/parser/response_parsing/introspection_spec.rb @@ -66,4 +66,20 @@ def on_headers_complete expect(@status_code).to eq(200) end end + + describe "keep_alive" do + let(:extension) { + proc { + def on_headers_complete + @context.instance_variable_set(:@keep_alive, @context.instance.keep_alive?) + end + } + } + + it "returns the keep alive state" do + parse + + expect(@keep_alive).to eq(true) + end + end end