├── lib ├── extpp │ ├── setup.rb │ ├── version.rb │ └── compiler.rb └── extpp.rb ├── .gitignore ├── sample └── hello │ ├── .gitignore │ ├── extconf.rb │ ├── hello.rb │ ├── Rakefile │ └── hello.cpp ├── Gemfile ├── benchmark ├── protect │ ├── extconf.rb │ ├── measure.rb │ └── protect.cpp └── define-method │ ├── extconf.rb │ ├── define.rb │ ├── call.rb │ └── define-method.cpp ├── test ├── fixtures │ ├── cast │ │ ├── extconf.rb │ │ └── cast.cpp │ ├── class │ │ ├── extconf.rb │ │ └── class.cpp │ ├── object │ │ ├── extconf.rb │ │ └── object.cpp │ └── protect │ │ ├── extconf.rb │ │ └── protect.cpp ├── helper.rb ├── run-test.rb ├── test-cast.rb ├── test-class.rb ├── test-protect.rb └── test-object.rb ├── include ├── ruby.hpp └── ruby │ ├── type.hpp │ ├── protect.hpp │ ├── function.hpp │ ├── cast.hpp │ ├── object.hpp │ └── class.hpp ├── Rakefile ├── extpp.gemspec ├── .github └── workflows │ ├── apache-arrow.yaml │ └── test.yaml ├── LICENSE.txt ├── README.md └── doc └── text └── news.md /lib/extpp/setup.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.so 3 | Makefile 4 | mkmf.log 5 | /pkg/ 6 | -------------------------------------------------------------------------------- /sample/hello/.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.so 3 | mkmf.log 4 | Makefile 5 | -------------------------------------------------------------------------------- /lib/extpp/version.rb: -------------------------------------------------------------------------------- 1 | module ExtPP 2 | VERSION = "0.1.2" 3 | end 4 | -------------------------------------------------------------------------------- /sample/hello/extconf.rb: -------------------------------------------------------------------------------- 1 | require "extpp" 2 | 3 | create_makefile("hello") 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # -*- ruby -*- 2 | 3 | source "https://rubygems.org" 4 | 5 | gemspec 6 | -------------------------------------------------------------------------------- /benchmark/protect/extconf.rb: -------------------------------------------------------------------------------- 1 | require "extpp" 2 | 3 | create_makefile("protect") 4 | -------------------------------------------------------------------------------- /benchmark/define-method/extconf.rb: -------------------------------------------------------------------------------- 1 | require "extpp" 2 | 3 | create_makefile("define_method") 4 | -------------------------------------------------------------------------------- /test/fixtures/cast/extconf.rb: -------------------------------------------------------------------------------- 1 | require "mkmf" 2 | require "extpp" 3 | 4 | create_makefile("cast") 5 | -------------------------------------------------------------------------------- /test/fixtures/class/extconf.rb: -------------------------------------------------------------------------------- 1 | require "mkmf" 2 | require "extpp" 3 | 4 | create_makefile("class") 5 | -------------------------------------------------------------------------------- /sample/hello/hello.rb: -------------------------------------------------------------------------------- 1 | require "extpp/setup" 2 | require "./hello.so" 3 | 4 | puts Hello.new("me").greet 5 | -------------------------------------------------------------------------------- /test/fixtures/object/extconf.rb: -------------------------------------------------------------------------------- 1 | require "mkmf" 2 | require "extpp" 3 | 4 | create_makefile("object") 5 | -------------------------------------------------------------------------------- /test/fixtures/protect/extconf.rb: -------------------------------------------------------------------------------- 1 | require "mkmf" 2 | require "extpp" 3 | 4 | create_makefile("protect") 5 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | require "test-unit" 2 | 3 | module Helper 4 | module Fixture 5 | def fixture_path(*components) 6 | File.join(__dir__, "fixtures", *components) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /include/ruby.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #define RB_BEGIN_DECLS extern "C" { 6 | #define RB_END_DECLS } 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | -------------------------------------------------------------------------------- /sample/hello/Rakefile: -------------------------------------------------------------------------------- 1 | task :default => :run 2 | 3 | desc "Run" 4 | task :run => :build do 5 | ruby("hello.rb") 6 | end 7 | 8 | desc "Build" 9 | task :build => "hello.so" 10 | 11 | file "Makefile" => "extconf.rb" do 12 | ruby("extconf.rb") 13 | sh("make", "clean") 14 | end 15 | 16 | file "hello.so" => ["Makefile", "hello.cpp"] do 17 | sh("make") 18 | end 19 | -------------------------------------------------------------------------------- /test/run-test.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $VERBOSE = true 4 | 5 | require "pathname" 6 | 7 | base_dir = Pathname.new(__dir__).parent.expand_path 8 | 9 | lib_dir = base_dir + "lib" 10 | test_dir = base_dir + "test" 11 | 12 | $LOAD_PATH.unshift(lib_dir.to_s) 13 | 14 | require_relative "helper" 15 | 16 | exit(Test::Unit::AutoRunner.run(true, test_dir.to_s)) 17 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # -*- ruby -*- 2 | 3 | require "rubygems" 4 | require "bundler/gem_helper" 5 | require "rake/clean" 6 | 7 | base_dir = File.join(File.dirname(__FILE__)) 8 | 9 | helper = Bundler::GemHelper.new(base_dir) 10 | def helper.version_tag 11 | version 12 | end 13 | 14 | helper.install 15 | 16 | desc "Run tests" 17 | task :test do 18 | ruby("test/run-test.rb") 19 | end 20 | 21 | task default: :test 22 | -------------------------------------------------------------------------------- /include/ruby/type.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace rb { 6 | using RawMethod = VALUE (*)(ANYARGS); 7 | 8 | using MethodWithoutArguments = VALUE (*)(VALUE self); 9 | using MethodWithArguments = VALUE (*)(VALUE self, int argc, VALUE *argv); 10 | using MethodWithArgumentsCompatible = 11 | VALUE (*)(int argc, VALUE *argv, VALUE self); 12 | 13 | using RawCallback = VALUE (*)(VALUE user_data); 14 | } 15 | -------------------------------------------------------------------------------- /sample/hello/hello.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | RB_BEGIN_DECLS 4 | 5 | void 6 | Init_hello(void) 7 | { 8 | rb::Class klass("Hello"); 9 | klass.define_method("initialize", [](VALUE rb_self, int argc, VALUE *argv) { 10 | VALUE rb_name; 11 | rb_scan_args(argc, argv, "1", &rb_name); 12 | rb::Object self(rb_self); 13 | self.ivar_set("@name", rb_name); 14 | return Qnil; 15 | }); 16 | klass.define_method("greet", [](VALUE rb_self) { 17 | rb::Object self(rb_self); 18 | rb::Object prefix(rb_str_new_cstr("Hello ")); 19 | auto message = prefix.send("+", {self.ivar_get("@name")}); 20 | return static_cast(message); 21 | }); 22 | } 23 | 24 | RB_END_DECLS 25 | -------------------------------------------------------------------------------- /benchmark/protect/measure.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "benchmark" 4 | require_relative "protect" 5 | 6 | GC.disable 7 | 8 | N = 100000 9 | 10 | Benchmark.bmbm do |job| 11 | protect = Protect.new 12 | 13 | job.report("Nothing") do 14 | protect.protect_nothing(N) 15 | end 16 | 17 | job.report("Nothing (closure)") do 18 | protect.protect_nothing(N) 19 | end 20 | 21 | job.report("C") do 22 | protect.protect_c(N) 23 | end 24 | 25 | job.report("C++") do 26 | protect.protect_cpp(N) 27 | end 28 | 29 | job.report("C++ (wrap)") do 30 | protect.protect_cpp_wrap(N) 31 | end 32 | 33 | job.report("C++ (closure)") do 34 | protect.protect_cpp_closure(N) 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /benchmark/define-method/define.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "benchmark" 4 | require_relative "define_method" 5 | 6 | n = 10000 7 | Benchmark.bmbm do |job| 8 | job.report("Ruby") do 9 | Class.new do 10 | n.times do |i| 11 | define_method("method#{i}") do 12 | end 13 | end 14 | end 15 | end 16 | 17 | job.report("C") do 18 | DefineMethodC.new.define(n, Class.new) 19 | end 20 | 21 | job.report("C++") do 22 | DefineMethodCPP.new.define(n, Class.new) 23 | end 24 | 25 | job.report("C++ (lazy)") do 26 | DefineMethodCPP.new.define_lazy(n, Class.new) 27 | end 28 | 29 | job.report("C++ (defined)") do 30 | DefineMethodCPP.new.define_defined(n, Class.new) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/extpp.rb: -------------------------------------------------------------------------------- 1 | require "extpp/compiler" 2 | 3 | compiler = ExtPP::Compiler.new(RbConfig.expand($CXXFLAGS)) 4 | compiler.check 5 | $CXXFLAGS = compiler.cxx_flags 6 | 7 | include_path = File.expand_path(File.join(__dir__, "..", "include")) 8 | $INCFLAGS += " -I#{include_path.quote}" 9 | 10 | header_files = Dir.chdir(include_path) do 11 | Dir.glob("**/*.*").collect do |path| 12 | File.join(include_path, path) 13 | end 14 | end 15 | header_files_dependency_injector = Module.new do 16 | define_method(:create_makefile) do |*args, &block| 17 | super(*args, &block) 18 | File.open("Makefile", "ab") do |makefile| 19 | makefile.print <<-MAKEFILE 20 | 21 | extpp_headers = #{header_files.join(" ")} 22 | $(OBJS): $(extpp_headers) 23 | MAKEFILE 24 | end 25 | end 26 | end 27 | extend(header_files_dependency_injector) 28 | -------------------------------------------------------------------------------- /extpp.gemspec: -------------------------------------------------------------------------------- 1 | # -*- ruby -*- 2 | 3 | clean_white_space = lambda do |entry| 4 | entry.gsub(/(\A\n+|\n+\z)/, '') + "\n" 5 | end 6 | 7 | require_relative "lib/extpp/version" 8 | 9 | Gem::Specification.new do |spec| 10 | spec.name = "extpp" 11 | spec.version = ExtPP::VERSION 12 | spec.homepage = "https://github.com/red-data-tools/extpp" 13 | spec.authors = ["Kouhei Sutou"] 14 | spec.email = ["kou@clear-code.com"] 15 | readme = File.read("README.md") 16 | readme.force_encoding("UTF-8") 17 | entries = readme.split(/^\#\#\s(.*)$/) 18 | description = clean_white_space.call(entries[entries.index("Description") + 1]) 19 | spec.summary, spec.description, = description.split(/\n\n+/, 3) 20 | spec.licenses = ["BSD-2-Clause"] 21 | spec.files = ["README.md", "Rakefile"] 22 | spec.files += ["LICENSE.txt"] 23 | spec.files += Dir.glob("lib/**/*.rb") 24 | spec.files += Dir.glob("include/**/*.hpp") 25 | spec.files += Dir.glob("doc/text/**/*.*") 26 | spec.files += Dir.glob("sample/hello/{Rakefile,extconf.rb,hello.cpp}") 27 | 28 | spec.add_development_dependency("bundler") 29 | spec.add_development_dependency("rake") 30 | spec.add_development_dependency("test-unit") 31 | end 32 | -------------------------------------------------------------------------------- /include/ruby/protect.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace rb { 6 | class State { 7 | public: 8 | explicit State(int state) : 9 | state_(state) { 10 | } 11 | 12 | [[noreturn]] inline void jump() { 13 | rb_jump_tag(state_); 14 | } 15 | 16 | private: 17 | int state_; 18 | }; 19 | 20 | inline VALUE protect(RawCallback callback, VALUE callback_data) { 21 | int state = 0; 22 | auto result = rb_protect(callback, callback_data, &state); 23 | if (state != 0) { 24 | throw State(state); 25 | } 26 | return result; 27 | } 28 | 29 | template 30 | VALUE protect(const NoArgumentCallback& callback) { 31 | struct Data { 32 | Data(const NoArgumentCallback& callback) : 33 | callback_(callback) { 34 | } 35 | const NoArgumentCallback& callback_; 36 | } data(callback); 37 | auto callback_data = reinterpret_cast(&data); 38 | return protect([](VALUE callback_data) -> VALUE { 39 | auto data = reinterpret_cast(callback_data); 40 | return data->callback_(); 41 | }, 42 | callback_data); 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /test/test-cast.rb: -------------------------------------------------------------------------------- 1 | class CastTest < Test::Unit::TestCase 2 | extend Helper::Fixture 3 | 4 | class << self 5 | def startup 6 | lib_dir = File.join(__dir__, "..", "lib") 7 | Dir.chdir(fixture_path("cast")) do 8 | system("make", "distclean") if File.exist?("Makefile") 9 | system(RbConfig.ruby, "-w", "-I", lib_dir, "extconf.rb") 10 | system("make") 11 | end 12 | require fixture_path("cast", "cast") 13 | end 14 | end 15 | 16 | def setup 17 | @caster = Caster.new 18 | end 19 | 20 | def test_int32 21 | assert_equal(-(2 ** 31), 22 | @caster.cast_int32(-(2 ** 31))) 23 | end 24 | 25 | def test_int64 26 | assert_equal(-(2 ** 63), 27 | @caster.cast_int64(-(2 ** 63))) 28 | end 29 | 30 | def test_uint32 31 | assert_equal(2 ** 32 - 1, 32 | @caster.cast_uint32(2 ** 32 - 1)) 33 | end 34 | 35 | def test_uint64 36 | assert_equal(2 ** 64 - 1, 37 | @caster.cast_uint64(2 ** 64 - 1)) 38 | end 39 | 40 | def test_string 41 | assert_equal("Hello", @caster.cast_string("Hello")) 42 | end 43 | 44 | def test_std_string 45 | assert_equal("Hello", @caster.cast_std_string("Hello")) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /.github/workflows/apache-arrow.yaml: -------------------------------------------------------------------------------- 1 | name: Apache Arrow 2 | on: 3 | push: 4 | pull_request: 5 | jobs: 6 | test: 7 | name: ${{ matrix.runs-on }} 8 | runs-on: ${{ matrix.runs-on }} 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | runs-on: 13 | - macos-latest 14 | - ubuntu-latest 15 | - windows-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: ruby/setup-ruby@v1 19 | with: 20 | ruby-version: "3.1" 21 | - name: Enable Apache Arrow repository 22 | if: | 23 | matrix.runs-on == 'ubuntu-latest' 24 | run: | 25 | sudo apt update 26 | sudo apt install -y -V \ 27 | lsb-release \ 28 | wget 29 | wget https://apache.jfrog.io/artifactory/arrow/$(lsb_release --id --short | tr 'A-Z' 'a-z')/apache-arrow-apt-source-latest-$(lsb_release --codename --short).deb 30 | sudo apt install -y -V ./apache-arrow-apt-source-latest-$(lsb_release --codename --short).deb 31 | sudo apt update 32 | - name: Install Ext++ 33 | run: rake install 34 | - name: Install Red Arrow 35 | run: gem install red-arrow 36 | - name: Load Red Arrow 37 | run: ruby -e "require 'arrow'" 38 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 2017-2021 Sutou Kouhei. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions 5 | are met: 6 | 1. Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | 2. Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 14 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 15 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 16 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 17 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 18 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 19 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 20 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 21 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 22 | SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /benchmark/define-method/call.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "benchmark" 4 | require_relative "define_method" 5 | 6 | N = 10000 7 | 8 | class RubyMethods 9 | N.times do |i| 10 | define_method("method#{i}") do 11 | end 12 | end 13 | end 14 | 15 | class CMethods 16 | DefineMethodC.new.define(N, self) 17 | end 18 | 19 | class CppMethods 20 | DefineMethodCPP.new.define(N, self) 21 | end 22 | 23 | class CppLazyMethods 24 | DefineMethodCPP.new.define_lazy(N, self) 25 | end 26 | 27 | class CppDefinedMethods 28 | DefineMethodCPP.new.define_defined(N, self) 29 | end 30 | 31 | Benchmark.bmbm do |job| 32 | ruby_object = RubyMethods.new 33 | job.report("Ruby") do 34 | N.times do |i| 35 | ruby_object.__send__("method#{i}") 36 | end 37 | end 38 | 39 | c_object = CMethods.new 40 | job.report("C") do 41 | N.times do |i| 42 | c_object.__send__("method#{i}") 43 | end 44 | end 45 | 46 | cpp_object = CppMethods.new 47 | job.report("C++") do 48 | N.times do |i| 49 | cpp_object.__send__("method#{i}") 50 | end 51 | end 52 | 53 | cpp_lazy_object = CppLazyMethods.new 54 | job.report("C++ (lazy)") do 55 | N.times do |i| 56 | cpp_lazy_object.__send__("method#{i}") 57 | end 58 | end 59 | 60 | cpp_defined_object = CppDefinedMethods.new 61 | job.report("C++ (defined)") do 62 | N.times do |i| 63 | cpp_defined_object.__send__("method#{i}") 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /test/test-class.rb: -------------------------------------------------------------------------------- 1 | class ClassTest < Test::Unit::TestCase 2 | extend Helper::Fixture 3 | 4 | class << self 5 | def startup 6 | return unless self == ClassTest 7 | lib_dir = File.join(__dir__, "..", "lib") 8 | Dir.chdir(fixture_path("class")) do 9 | system("make", "distclean") if File.exist?("Makefile") 10 | system(RbConfig.ruby, "-w", "-I", lib_dir, "extconf.rb") 11 | system("make") 12 | end 13 | require fixture_path("class", "class") 14 | end 15 | end 16 | 17 | sub_test_case("no argument") do 18 | def test_define_method 19 | assert_equal("Hello", Greeting.new.hello) 20 | end 21 | 22 | def test_define_method_lazy 23 | assert_equal("Hello", Greeting.new.hello_lazy) 24 | end 25 | 26 | def test_define_method_defined 27 | assert_equal("Hello", Greeting.new.hello_defined) 28 | end 29 | end 30 | 31 | sub_test_case("with arguments") do 32 | def test_define_method 33 | assert_equal("Hello Ruby", NamedGreeting.new.hello("Ruby")) 34 | end 35 | 36 | def test_define_method_compatible 37 | assert_equal("Hello Ruby", NamedGreeting.new.hello_compatible("Ruby")) 38 | end 39 | 40 | def test_define_method_lazy 41 | assert_equal("Hello Ruby", NamedGreeting.new.hello_lazy("Ruby")) 42 | end 43 | 44 | def test_define_method_defined 45 | assert_equal("Hello Ruby", NamedGreeting.new.hello_defined("Ruby")) 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /test/test-protect.rb: -------------------------------------------------------------------------------- 1 | class ProtectTest < Test::Unit::TestCase 2 | extend Helper::Fixture 3 | 4 | class << self 5 | def startup 6 | return unless self == ProtectTest 7 | lib_dir = File.join(__dir__, "..", "lib") 8 | Dir.chdir(fixture_path("protect")) do 9 | system("make", "distclean") if File.exist?("Makefile") 10 | system(RbConfig.ruby, "-w", "-I", lib_dir, "extconf.rb") 11 | system("make") 12 | end 13 | require fixture_path("protect", "protect") 14 | end 15 | end 16 | 17 | class Listener 18 | attr_reader :events 19 | def initialize 20 | @events = [] 21 | end 22 | 23 | def notify(event) 24 | @events << event 25 | end 26 | end 27 | 28 | def setup 29 | @protect_methods = ProtectMethods.new 30 | end 31 | 32 | def assert_protect(method_name) 33 | listener = Listener.new 34 | assert_raise(RuntimeError.new("message")) do 35 | @protect_methods.__send__(method_name, listener) do 36 | raise "message" 37 | end 38 | end 39 | assert_equal([ 40 | "outer: constructed", 41 | "inner: constructed", 42 | "inner: destructed", 43 | "outer: caught", 44 | ], 45 | listener.events) 46 | end 47 | 48 | def test_raw 49 | assert_protect(:protect_raw) 50 | end 51 | 52 | def test_closure 53 | assert_protect(:protect_closure) 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /include/ruby/function.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace rb { 6 | class Function { 7 | public: 8 | Function() = default; 9 | virtual ~Function() = default; 10 | 11 | virtual VALUE call(VALUE self, int argc, VALUE *argv) = 0; 12 | }; 13 | 14 | class FunctionWithoutArgument : public Function { 15 | public: 16 | FunctionWithoutArgument(const MethodWithoutArguments &function) : 17 | function_(function) { 18 | } 19 | 20 | inline VALUE call(VALUE self, int argc, VALUE *argv) override { 21 | return function_(self); 22 | } 23 | 24 | private: 25 | MethodWithoutArguments function_; 26 | }; 27 | 28 | class FunctionWithArguments : public Function { 29 | public: 30 | FunctionWithArguments(const MethodWithArguments &function) : 31 | function_(function) { 32 | } 33 | 34 | inline VALUE call(VALUE self, int argc, VALUE *argv) override { 35 | return function_(self, argc, argv); 36 | } 37 | 38 | private: 39 | MethodWithArguments function_; 40 | }; 41 | 42 | class FunctionWithArgumentsCompatible : public Function { 43 | public: 44 | FunctionWithArgumentsCompatible( 45 | const MethodWithArgumentsCompatible &function) : 46 | function_(function) { 47 | } 48 | 49 | inline VALUE call(VALUE self, int argc, VALUE *argv) override { 50 | return function_(argc, argv, self); 51 | } 52 | 53 | private: 54 | MethodWithArgumentsCompatible function_; 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /test/fixtures/class/class.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | namespace { 4 | VALUE rb_hello(VALUE self) { 5 | return rb_str_new_cstr("Hello"); 6 | } 7 | 8 | VALUE rb_named_hello(int argc, VALUE *argv, VALUE self) { 9 | VALUE rb_name; 10 | rb_scan_args(argc, argv, "10", &rb_name); 11 | return rb_str_plus(rb_str_new_cstr("Hello "), rb_name); 12 | } 13 | } 14 | 15 | extern "C" void 16 | Init_class(void) 17 | { 18 | rb::Class("Greeting"). 19 | define_method("hello", [](VALUE self) { 20 | return rb_str_new_cstr("Hello"); 21 | }). 22 | enable_lazy_define_method(). 23 | define_method("hello_lazy", [](VALUE self) { 24 | return rb_str_new_cstr("Hello"); 25 | }). 26 | define_method("hello_defined", rb_hello); 27 | 28 | rb::Class("NamedGreeting"). 29 | define_method("hello", [](VALUE self, int argc, VALUE *argv) { 30 | VALUE rb_name; 31 | rb_scan_args(argc, argv, "10", &rb_name); 32 | return rb_str_plus(rb_str_new_cstr("Hello "), rb_name); 33 | }). 34 | define_method("hello_compatible", [](int argc, VALUE *argv, VALUE self) { 35 | VALUE rb_name; 36 | rb_scan_args(argc, argv, "10", &rb_name); 37 | return rb_str_plus(rb_str_new_cstr("Hello "), rb_name); 38 | }). 39 | enable_lazy_define_method(). 40 | define_method("hello_lazy", [](VALUE self, int argc, VALUE *argv) { 41 | VALUE rb_name; 42 | rb_scan_args(argc, argv, "10", &rb_name); 43 | return rb_str_plus(rb_str_new_cstr("Hello "), rb_name); 44 | }). 45 | define_method("hello_defined", rb_named_hello); 46 | } 47 | -------------------------------------------------------------------------------- /test/fixtures/cast/cast.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | extern "C" void 4 | Init_cast(void) 5 | { 6 | rb::Class("Caster"). 7 | define_method("cast_int32", [](VALUE self, int argc, VALUE *argv) -> VALUE { 8 | VALUE rb_n; 9 | rb_scan_args(argc, argv, "1", &rb_n); 10 | return rb::cast(rb::cast(rb::Object(rb_n))); 11 | }). 12 | define_method("cast_int64", [](VALUE self, int argc, VALUE *argv) -> VALUE { 13 | VALUE rb_n; 14 | rb_scan_args(argc, argv, "1", &rb_n); 15 | return rb::cast(rb::cast(rb::Object(rb_n))); 16 | }). 17 | define_method("cast_uint32", [](VALUE self, int argc, VALUE *argv) -> VALUE { 18 | VALUE rb_n; 19 | rb_scan_args(argc, argv, "1", &rb_n); 20 | return rb::cast(rb::cast(rb::Object(rb_n))); 21 | }). 22 | define_method("cast_uint64", [](VALUE self, int argc, VALUE *argv) -> VALUE { 23 | VALUE rb_n; 24 | rb_scan_args(argc, argv, "1", &rb_n); 25 | return rb::cast(rb::cast(rb::Object(rb_n))); 26 | }). 27 | define_method("cast_string", [](VALUE self, int argc, VALUE *argv) -> VALUE { 28 | VALUE rb_string; 29 | rb_scan_args(argc, argv, "1", &rb_string); 30 | return rb::cast(rb::cast(rb::Object(rb_string))); 31 | }). 32 | define_method("cast_std_string", 33 | [](VALUE self, int argc, VALUE *argv) -> VALUE { 34 | VALUE rb_string; 35 | rb_scan_args(argc, argv, "1", &rb_string); 36 | const auto std_string = rb::cast(rb::Object(rb_string)); 37 | return rb::cast(std_string); 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | pull_request: 5 | jobs: 6 | test: 7 | name: ${{ matrix.ruby-version }} on ${{ matrix.runs-on }} 8 | runs-on: ${{ matrix.runs-on }} 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | ruby-version: 13 | - "2.7" 14 | - "3.0" 15 | - "3.1" 16 | runs-on: 17 | - macos-latest 18 | - ubuntu-latest 19 | - windows-latest 20 | steps: 21 | - uses: actions/checkout@v3 22 | - uses: ruby/setup-ruby@v1 23 | with: 24 | ruby-version: ${{ matrix.ruby-version }} 25 | bundler-cache: true 26 | - run: bundle exec rake 27 | 28 | xcode: 29 | name: Xcode ${{ matrix.xcode-version }} on macOS ${{ matrix.macos-version }} 30 | runs-on: macos-${{ matrix.macos-version }} 31 | strategy: 32 | fail-fast: false 33 | matrix: 34 | include: 35 | - xcode-version: "11.7" 36 | macos-version: "11" 37 | - xcode-version: "12.4" 38 | macos-version: "11" 39 | - xcode-version: "12.5" 40 | macos-version: "11" 41 | - xcode-version: "13.0" 42 | macos-version: "11" 43 | - xcode-version: "13.1" 44 | macos-version: "11" 45 | - xcode-version: "13.2" 46 | macos-version: "11" 47 | - xcode-version: "13.1" 48 | macos-version: "12" 49 | - xcode-version: "13.2" 50 | macos-version: "12" 51 | - xcode-version: "13.3" 52 | macos-version: "12" 53 | - xcode-version: "13.4" 54 | macos-version: "12" 55 | - xcode-version: "14.0" 56 | macos-version: "12" 57 | - xcode-version: "14.1" 58 | macos-version: "12" 59 | steps: 60 | - uses: actions/checkout@v3 61 | - uses: maxim-lobanov/setup-xcode@v1 62 | with: 63 | xcode-version: ${{ matrix.xcode-version }} 64 | - uses: ruby/setup-ruby@v1 65 | with: 66 | ruby-version: "3.1" 67 | bundler-cache: true 68 | - run: bundle exec rake 69 | -------------------------------------------------------------------------------- /test/test-object.rb: -------------------------------------------------------------------------------- 1 | class ObjectTest < Test::Unit::TestCase 2 | extend Helper::Fixture 3 | 4 | class << self 5 | def startup 6 | return unless self == ObjectTest 7 | lib_dir = File.join(__dir__, "..", "lib") 8 | Dir.chdir(fixture_path("object")) do 9 | system("make", "distclean") if File.exist?("Makefile") 10 | system(RbConfig.ruby, "-w", "-I", lib_dir, "extconf.rb") 11 | system("make") 12 | end 13 | require fixture_path("object", "object") 14 | end 15 | end 16 | 17 | sub_test_case("to_bool") do 18 | def test_true 19 | assert_equal(true, ObjectMethods.new.to_bool_true) 20 | end 21 | 22 | def test_false 23 | assert_equal(false, ObjectMethods.new.to_bool_false) 24 | end 25 | 26 | def test_nil 27 | assert_equal(false, ObjectMethods.new.to_bool_nil) 28 | end 29 | end 30 | 31 | def test_to_ruby 32 | assert_equal("Hello", ObjectMethods.new.to_ruby("Hello")) 33 | end 34 | 35 | sub_test_case("send") do 36 | def test_no_arguments 37 | assert_equal("1", ObjectMethods.new.send_no_arguments(1, "to_s")) 38 | end 39 | 40 | def test_two_arguments 41 | assert_equal("ell", 42 | ObjectMethods.new.send_two_arguments("hello", 43 | "[]", 44 | 1, 45 | 3)) 46 | end 47 | 48 | def test_block 49 | assert_equal([1, 4, 9], 50 | ObjectMethods.new.send_block([1, 2, 3], "collect")) 51 | end 52 | end 53 | 54 | sub_test_case("instance variable") do 55 | def test_set 56 | object = Object.new 57 | methods = ObjectMethods.new 58 | assert_equal("me", methods.ivar_set(object, "@name", "me")) 59 | assert_equal("me", object.instance_variable_get("@name")) 60 | end 61 | 62 | def test_get 63 | object = Object.new 64 | object.instance_variable_set("@name", "me") 65 | methods = ObjectMethods.new 66 | assert_equal("me", methods.ivar_get(object, "@name")) 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /test/fixtures/protect/protect.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | namespace { 4 | class Notifier { 5 | public: 6 | Notifier(std::string prefix, rb::Object listener) : 7 | prefix_(prefix), 8 | listener_(listener) { 9 | notify("constructed"); 10 | } 11 | 12 | ~Notifier() { 13 | notify("destructed"); 14 | } 15 | 16 | void notify(const char* name) { 17 | auto event = prefix_ + name; 18 | auto x = rb::Object(rb_str_new(event.data(), event.size())); 19 | listener_.send("notify", {x}); 20 | } 21 | 22 | private: 23 | std::string prefix_; 24 | rb::Object listener_; 25 | }; 26 | }; 27 | 28 | extern "C" void 29 | Init_protect(void) 30 | { 31 | rb::Class klass("ProtectMethods"); 32 | klass.define_method("protect_raw", [](int argc, VALUE *argv, VALUE self) { 33 | VALUE rb_listener; 34 | VALUE rb_block; 35 | rb_scan_args(argc, argv, "1&", &rb_listener, &rb_block); 36 | Notifier outer("outer: ", rb::Object(rb_listener)); 37 | try { 38 | Notifier inner("inner: ", rb::Object(rb_listener)); 39 | rb::protect( 40 | [](VALUE rb_block) -> VALUE { 41 | return rb::Object(rb_block).send("call"); 42 | }, 43 | rb_block); 44 | inner.notify("called"); 45 | } catch (rb::State& state) { 46 | outer.notify("caught"); 47 | state.jump(); 48 | } 49 | outer.notify("jumped"); 50 | return Qnil; 51 | }); 52 | klass.define_method("protect_closure", [](int argc, VALUE *argv, VALUE self) { 53 | VALUE rb_listener; 54 | VALUE rb_block; 55 | rb_scan_args(argc, argv, "1&", &rb_listener, &rb_block); 56 | Notifier outer("outer: ", rb::Object(rb_listener)); 57 | try { 58 | Notifier inner("inner: ", rb::Object(rb_listener)); 59 | rb::protect([&rb_block]() -> VALUE { 60 | return rb::Object(rb_block).send("call"); 61 | }); 62 | inner.notify("called"); 63 | } catch (rb::State& state) { 64 | outer.notify("caught"); 65 | state.jump(); 66 | } 67 | outer.notify("jumped"); 68 | return Qnil; 69 | }); 70 | } 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # README 2 | 3 | ## Name 4 | 5 | Ext++ 6 | 7 | ## Description 8 | 9 | Ext++ is a Ruby extension that provides C++ API for writing Ruby extension. 10 | 11 | You can write your Ruby extension easier than Ruby's C API. Because 12 | Ext++'s C++ API reduces duplicated code needed for Ruby's C API. 13 | 14 | You can use all Ruby's C API without any adapter layer. Because you 15 | can use C API directory from C++. 16 | 17 | ## Install 18 | 19 | ```console 20 | % gem install extpp 21 | ``` 22 | 23 | ## How to use 24 | 25 | Here is an example to create `Hello` class. 26 | 27 | Create `hello.cpp`: 28 | 29 | ```c++ 30 | #include 31 | 32 | RB_BEGIN_DECLS 33 | 34 | void 35 | Init_hello(void) 36 | { 37 | rb::Class klass("Hello"); 38 | klass.define_method("initialize", [](VALUE rb_self, int argc, VALUE *argv) { 39 | VALUE rb_name; 40 | rb_scan_args(argc, argv, "1", &rb_name); 41 | rb::Object self(rb_self); 42 | self.ivar_set("@name", rb_name); 43 | return Qnil; 44 | }); 45 | klass.define_method("greet", [](VALUE rb_self) { 46 | rb::Object self(rb_self); 47 | rb::Object prefix(rb_str_new_cstr("Hello ")); 48 | auto message = prefix.send("+", {self.ivar_get("@name")}); 49 | return static_cast(message); 50 | }); 51 | } 52 | 53 | RB_END_DECLS 54 | ``` 55 | 56 | This code equals to the following Ruby code: 57 | 58 | ```ruby 59 | class Hello 60 | def initialize(name) 61 | @name = name 62 | end 63 | 64 | def greet 65 | "Hello " + @name 66 | end 67 | end 68 | ``` 69 | 70 | Create `extconf.rb`: 71 | 72 | ```ruby 73 | require "extpp" 74 | 75 | create_makefile("hello") 76 | ``` 77 | 78 | Create `Makefile`: 79 | 80 | ```console 81 | % ruby extconf.rb 82 | ``` 83 | 84 | Build this extension: 85 | 86 | ```console 87 | % make 88 | ``` 89 | 90 | Now, you can use this extension: 91 | 92 | ```console 93 | % ruby -r ./hello -e 'p Hello.new("me").greet' 94 | "Hello me" 95 | ``` 96 | 97 | ## License 98 | 99 | Copyright (C) 2017-2021 Sutou Kouhei 100 | 101 | The 2-Clause BSD License. See [LICENSE.txt](LICENSE.txt) for details. 102 | -------------------------------------------------------------------------------- /doc/text/news.md: -------------------------------------------------------------------------------- 1 | # News 2 | 3 | ## 0.1.1 - 2022-11-02 4 | 5 | ### Improvements 6 | 7 | * Removed needless a workaround for old Ruby with C++17. 8 | 9 | ## 0.1.0 - 2021-10-03 10 | 11 | ### Improvements 12 | 13 | * Implemented as header only library. 14 | 15 | ## 0.0.9 - 2020-11-03 16 | 17 | ### Improvements 18 | 19 | * Improved `clang++` version detection. 20 | [GitHub#10][Patch by Alex Neill] 21 | 22 | * Improved `g++` version detection. 23 | 24 | ### Thanks 25 | 26 | * Alex Neill 27 | 28 | ## 0.0.8 - 2019-09-25 29 | 30 | ### Improvements 31 | 32 | * Changed to use `RbConfig::CONFIG["SOEXT"]`. 33 | [Suggested by Nobuyoshi Nakada] 34 | 35 | * Added `ExtpPP::Platform#shared_library_extension`. 36 | 37 | * Added support for macOS Majave. 38 | [GitHub#8][Patch by Josh Huckabee] 39 | 40 | ### Thanks 41 | 42 | * Nobuyoshi Nakada 43 | 44 | * Josh Huckabee 45 | 46 | ## 0.0.7 - 2019-03-21 47 | 48 | ### Improvements 49 | 50 | * Added support for Windows. 51 | 52 | ## 0.0.6 - 2019-02-25 53 | 54 | ### Improvements 55 | 56 | * Suppressed warnings by `register`. 57 | [GitHub#6][Patch by Kenta Murata] 58 | 59 | * Added `noreturn` attribute to suppress warning. 60 | [GitHub#7][Patch by Kenta Murata] 61 | 62 | ### Thanks 63 | 64 | * Kenta Murata 65 | 66 | ## 0.0.5 - 2019-02-25 67 | 68 | ### Improvements 69 | 70 | * Added support for Xcode 10. 71 | [GitHub#4][Reported by Kenta Murata] 72 | 73 | ### Thanks 74 | 75 | * Kenta Murata 76 | 77 | ## 0.0.4 - 2019-02-16 78 | 79 | ### Improvements 80 | 81 | * `rb::Class(const char *name)`: Added. 82 | 83 | * `rb::protect()`: Added. 84 | 85 | * `rb::cast(rb::Object)`: Added. 86 | 87 | * `rb::cast 2 | 3 | RB_BEGIN_DECLS 4 | 5 | static VALUE 6 | rb_method(VALUE self) 7 | { 8 | return Qnil; 9 | } 10 | 11 | static VALUE 12 | rb_define(VALUE self, VALUE rb_n, VALUE rb_klass) 13 | { 14 | auto n = NUM2INT(rb_n); 15 | for (int i = 0; i < n; ++i) { 16 | char method_name[256]; 17 | snprintf(method_name, sizeof(method_name), "method%d", i); 18 | rb_define_method(rb_klass, 19 | method_name, 20 | reinterpret_cast(rb_method), 21 | 0); 22 | } 23 | return Qnil; 24 | } 25 | 26 | void 27 | Init_define_method(void) 28 | { 29 | { 30 | auto klass = rb_define_class("DefineMethodC", rb_cObject); 31 | rb_define_method(klass, 32 | "define", 33 | reinterpret_cast(rb_define), 34 | 2); 35 | } 36 | 37 | { 38 | rb::Class("DefineMethodCPP", rb_cObject). 39 | define_method("define", [](VALUE self, int argc, VALUE *argv) { 40 | VALUE rb_n; 41 | VALUE rb_klass; 42 | rb_scan_args(argc, argv, "2", &rb_n, &rb_klass); 43 | auto n = NUM2INT(rb_n); 44 | rb::Class klass(rb_klass); 45 | for (int i = 0; i < n; ++i) { 46 | char method_name[256]; 47 | snprintf(method_name, sizeof(method_name), "method%d", i); 48 | klass.define_method(method_name, [](VALUE self) { 49 | return Qnil; 50 | }); 51 | } 52 | return Qnil; 53 | }). 54 | define_method("define_lazy", [](VALUE self, int argc, VALUE *argv) { 55 | VALUE rb_n; 56 | VALUE rb_klass; 57 | rb_scan_args(argc, argv, "2", &rb_n, &rb_klass); 58 | auto n = NUM2INT(rb_n); 59 | rb::Class klass(rb_klass); 60 | klass.enable_lazy_define_method(); 61 | for (int i = 0; i < n; ++i) { 62 | char method_name[256]; 63 | snprintf(method_name, sizeof(method_name), "method%d", i); 64 | klass.define_method(method_name, [](VALUE self) { 65 | return Qnil; 66 | }); 67 | } 68 | return Qnil; 69 | }). 70 | define_method("define_defined", [](VALUE self, int argc, VALUE *argv) { 71 | VALUE rb_n; 72 | VALUE rb_klass; 73 | rb_scan_args(argc, argv, "2", &rb_n, &rb_klass); 74 | auto n = NUM2INT(rb_n); 75 | rb::Class klass(rb_klass); 76 | for (int i = 0; i < n; ++i) { 77 | char method_name[256]; 78 | snprintf(method_name, sizeof(method_name), "method%d", i); 79 | klass.define_method(method_name, rb_method); 80 | } 81 | return Qnil; 82 | }); 83 | } 84 | } 85 | 86 | RB_END_DECLS 87 | -------------------------------------------------------------------------------- /include/ruby/cast.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | 7 | namespace rb { 8 | template 9 | inline RETURN_TYPE cast(const ARGUMENT_TYPE& object); 10 | 11 | template 12 | inline RETURN_TYPE cast(const ARGUMENT_TYPE *object); 13 | 14 | template 17 | inline RETURN_TYPE cast(const ARGUMENT_TYPE& object, 18 | ADDTIONAL_DATA_TYPE1 data1); 19 | 20 | template 23 | inline RETURN_TYPE cast(const ARGUMENT_TYPE *object, 24 | ADDTIONAL_DATA_TYPE1 data1); 25 | 26 | template <> 27 | inline int32_t cast(const Object& rb_object) { 28 | return NUM2INT(rb_object); 29 | } 30 | 31 | template <> 32 | inline Object cast(const int32_t& n) { 33 | return Object(INT2NUM(n)); 34 | } 35 | 36 | 37 | template <> 38 | inline int64_t cast(const Object& rb_object) { 39 | return NUM2LL(rb_object); 40 | } 41 | 42 | template <> 43 | inline Object cast(const int64_t& n) { 44 | return Object(LL2NUM(n)); 45 | } 46 | 47 | 48 | template <> 49 | inline uint32_t cast(const Object& rb_object) { 50 | return NUM2UINT(rb_object); 51 | } 52 | 53 | template <> 54 | inline Object cast(const uint32_t& n) { 55 | return Object(UINT2NUM(n)); 56 | } 57 | 58 | 59 | template <> 60 | inline uint64_t cast(const Object& rb_object) { 61 | return NUM2ULL(rb_object); 62 | } 63 | 64 | template <> 65 | inline Object cast(const uint64_t& n) { 66 | return Object(ULL2NUM(n)); 67 | } 68 | 69 | 70 | template <> 71 | inline const char *cast(const Object& rb_object) { 72 | VALUE rb_object_raw = rb_object; 73 | return StringValueCStr(rb_object_raw); 74 | } 75 | 76 | template <> 77 | inline Object cast(const char *c_string) { 78 | return Object(rb_str_new_cstr(c_string)); 79 | } 80 | 81 | template <> 82 | inline Object cast(const char *data, long size) { 83 | return Object(rb_str_new(data, size)); 84 | } 85 | 86 | 87 | template <> 88 | inline std::string cast(const Object& rb_object) { 89 | VALUE rb_object_raw = rb_object; 90 | return std::string(RSTRING_PTR(rb_object_raw), 91 | RSTRING_LEN(rb_object_raw)); 92 | } 93 | 94 | template <> 95 | inline Object cast(const std::string& string) { 96 | return Object(rb_str_new(string.data(), string.size())); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /benchmark/protect/protect.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | RB_BEGIN_DECLS 4 | 5 | static VALUE 6 | rb_do_nothing(VALUE user_data) 7 | { 8 | return Qnil; 9 | } 10 | 11 | static VALUE 12 | rb_protect_nothing(VALUE self, VALUE rb_n) 13 | { 14 | auto n = NUM2INT(rb_n); 15 | for (int i = 0; i < n; ++i) { 16 | rb_do_nothing(Qnil); 17 | } 18 | return Qnil; 19 | } 20 | 21 | static VALUE 22 | rb_protect_nothing_closure(VALUE self, VALUE rb_n) 23 | { 24 | auto n = NUM2INT(rb_n); 25 | VALUE user_data = Qnil; 26 | for (int i = 0; i < n; ++i) { 27 | ([&]() -> VALUE { 28 | return rb_do_nothing(user_data); 29 | })(); 30 | } 31 | return Qnil; 32 | } 33 | 34 | static VALUE 35 | rb_protect_c(VALUE self, VALUE rb_n) 36 | { 37 | auto n = NUM2INT(rb_n); 38 | for (int i = 0; i < n; ++i) { 39 | int state = 0; 40 | rb_protect(rb_do_nothing, Qnil, &state); 41 | if (state != 0) { 42 | rb_jump_tag(state); 43 | } 44 | } 45 | return Qnil; 46 | } 47 | 48 | static VALUE 49 | rb_protect_cpp(VALUE self, VALUE rb_n) 50 | { 51 | auto n = NUM2INT(rb_n); 52 | try { 53 | for (int i = 0; i < n; ++i) { 54 | rb::protect(rb_do_nothing, Qnil); 55 | } 56 | } catch (rb::State& state) { 57 | state.jump(); 58 | } 59 | return Qnil; 60 | } 61 | 62 | static VALUE 63 | rb_protect_cpp_wrap(VALUE self, VALUE rb_n) 64 | { 65 | auto n = NUM2INT(rb_n); 66 | try { 67 | for (int i = 0; i < n; ++i) { 68 | rb::protect([](VALUE user_data) -> VALUE { 69 | return rb_do_nothing(user_data); 70 | }, 71 | Qnil); 72 | } 73 | } catch (rb::State& state) { 74 | state.jump(); 75 | } 76 | return Qnil; 77 | } 78 | 79 | static VALUE 80 | rb_protect_cpp_closure(VALUE self, VALUE rb_n) 81 | { 82 | auto n = NUM2INT(rb_n); 83 | try { 84 | VALUE user_data = Qnil; 85 | for (int i = 0; i < n; ++i) { 86 | rb::protect([&]() -> VALUE { 87 | return rb_do_nothing(user_data); 88 | }); 89 | } 90 | } catch (rb::State& state) { 91 | state.jump(); 92 | } 93 | return Qnil; 94 | } 95 | 96 | void 97 | Init_protect(void) 98 | { 99 | auto klass = rb_define_class("Protect", rb_cObject); 100 | rb_define_method(klass, 101 | "protect_nothing", 102 | reinterpret_cast(rb_protect_nothing), 103 | 1); 104 | rb_define_method(klass, 105 | "protect_nothing_closure", 106 | reinterpret_cast(rb_protect_nothing_closure), 107 | 1); 108 | rb_define_method(klass, 109 | "protect_c", 110 | reinterpret_cast(rb_protect_c), 111 | 1); 112 | rb_define_method(klass, 113 | "protect_cpp", 114 | reinterpret_cast(rb_protect_cpp), 115 | 1); 116 | rb_define_method(klass, 117 | "protect_cpp_wrap", 118 | reinterpret_cast(rb_protect_cpp_wrap), 119 | 1); 120 | rb_define_method(klass, 121 | "protect_cpp_closure", 122 | reinterpret_cast(rb_protect_cpp_closure), 123 | 1); 124 | } 125 | 126 | RB_END_DECLS 127 | -------------------------------------------------------------------------------- /test/fixtures/object/object.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | extern "C" void 4 | Init_object(void) 5 | { 6 | rb::Class klass("ObjectMethods"); 7 | klass.define_method("to_bool_true", [](VALUE self) { 8 | rb::Object object(Qtrue); 9 | if (object) { 10 | return Qtrue; 11 | } else { 12 | return Qfalse; 13 | } 14 | }); 15 | klass.define_method("to_bool_false", [](VALUE self) { 16 | rb::Object object(Qfalse); 17 | if (object) { 18 | return Qtrue; 19 | } else { 20 | return Qfalse; 21 | } 22 | }); 23 | klass.define_method("to_bool_nil", [](VALUE self) { 24 | rb::Object object(Qnil); 25 | if (object) { 26 | return Qtrue; 27 | } else { 28 | return Qfalse; 29 | } 30 | }); 31 | klass.define_method("to_ruby", [](int argc, VALUE *argv, VALUE self) { 32 | VALUE rb_object; 33 | rb_scan_args(argc, argv, "1", &rb_object); 34 | rb::Object object(rb_object); 35 | return static_cast(object); 36 | }); 37 | klass.define_method("send_no_arguments", [](int argc, VALUE *argv, VALUE self) { 38 | VALUE rb_receiver; 39 | VALUE rb_method_name; 40 | rb_scan_args(argc, argv, "2", &rb_receiver, &rb_method_name); 41 | rb::Object receiver(rb_receiver); 42 | rb::Object method_name(rb_method_name); 43 | auto result = receiver.send(method_name); 44 | return static_cast(result); 45 | }); 46 | klass.define_method("send_two_arguments", [](int argc, VALUE *argv, VALUE self) { 47 | VALUE rb_receiver; 48 | VALUE rb_method_name; 49 | VALUE rb_arg1; 50 | VALUE rb_arg2; 51 | rb_scan_args(argc, argv, "4", 52 | &rb_receiver, 53 | &rb_method_name, 54 | &rb_arg1, 55 | &rb_arg2); 56 | rb::Object receiver(rb_receiver); 57 | rb::Object method_name(rb_method_name); 58 | auto result = receiver.send(method_name, {rb_arg1, rb_arg2}); 59 | return static_cast(result); 60 | }); 61 | klass.define_method("send_block", [](int argc, VALUE *argv, VALUE self) { 62 | VALUE rb_receiver; 63 | VALUE rb_method_name; 64 | rb_scan_args(argc, argv, "2", 65 | &rb_receiver, 66 | &rb_method_name); 67 | rb::Object receiver(rb_receiver); 68 | rb::Object method_name(rb_method_name); 69 | auto result = receiver.send(method_name, 70 | {}, 71 | [](VALUE rb_n) -> VALUE { 72 | auto n = rb::Object(rb_n); 73 | auto n_raw = rb::cast(n); 74 | return rb::cast(n_raw * n_raw); 75 | }); 76 | return static_cast(result); 77 | }); 78 | klass.define_method("ivar_set", [](int argc, VALUE *argv, VALUE self) { 79 | VALUE rb_receiver; 80 | VALUE rb_name; 81 | VALUE rb_value; 82 | rb_scan_args(argc, argv, "3", 83 | &rb_receiver, 84 | &rb_name, 85 | &rb_value); 86 | rb::Object receiver(rb_receiver); 87 | rb::Object name(rb_name); 88 | auto result = receiver.ivar_set(name, rb_value); 89 | return static_cast(result); 90 | }); 91 | klass.define_method("ivar_get", [](int argc, VALUE *argv, VALUE self) { 92 | VALUE rb_receiver; 93 | VALUE rb_name; 94 | rb_scan_args(argc, argv, "2", 95 | &rb_receiver, 96 | &rb_name); 97 | rb::Object receiver(rb_receiver); 98 | rb::Object name(rb_name); 99 | auto result = receiver.ivar_get(name); 100 | return static_cast(result); 101 | }); 102 | } 103 | -------------------------------------------------------------------------------- /lib/extpp/compiler.rb: -------------------------------------------------------------------------------- 1 | require "mkmf" 2 | 3 | module ExtPP 4 | class Compiler 5 | attr_reader :cxx_flags 6 | def initialize(cxx_flags) 7 | @cxx_flags = cxx_flags 8 | end 9 | 10 | def gcc? 11 | RbConfig::CONFIG["GCC"] == "yes" 12 | end 13 | 14 | def check 15 | check_debug_build 16 | check_cxx 17 | check_version 18 | check_warning_flags 19 | end 20 | 21 | private 22 | def check_debug_build 23 | checking_for(checking_message("--enable-debug-build option")) do 24 | enable_debug_build = enable_config("debug-build", false) 25 | if enable_debug_build 26 | @cxx_flags = disable_optimization_build_flag(@cxx_flags) 27 | @cxx_flags = enable_debug_build_flag(@cxx_flags) 28 | end 29 | enable_debug_build 30 | end 31 | end 32 | 33 | def disable_optimization_build_flag(flags) 34 | if gcc? 35 | flags.gsub(/(^|\s)-O\d(\s|$)/, '\\1-O0\\2') 36 | else 37 | flags 38 | end 39 | end 40 | 41 | def enable_debug_build_flag(flags) 42 | if gcc? 43 | flags.gsub(/(^|\s)(?:-g|-g\d|-ggdb\d?)(\s|$)/, '\\1-g3\\2') 44 | else 45 | flags 46 | end 47 | end 48 | 49 | def check_cxx 50 | checking_for(checking_message("C++ compiler"), "%s") do 51 | RbConfig.expand("$(CXX)") 52 | end 53 | end 54 | 55 | def check_version 56 | return unless gcc? 57 | 58 | checking_for(checking_message("g++ version"), "%g%s") do 59 | version = 0.0 60 | std = nil 61 | 62 | case `#{RbConfig.expand("$(CXX) --version")}` 63 | when /g\+\+.+ (\d+\.\d+)\.\d/ 64 | version = Float($1) 65 | if version < 5.1 66 | std = "gnu++11" 67 | elsif version < 6.1 68 | std = "gnu++14" 69 | else 70 | std = "gnu++17" 71 | end 72 | when /\AApple (?:LLVM|clang) version (\d+\.\d+)\.\d/ 73 | version = Float($1) 74 | # TODO: Is it right? 75 | if version < 9.0 76 | std = "gnu++11" 77 | elsif version < 9.1 78 | std = "gnu++14" 79 | else 80 | std = "gnu++17" 81 | end 82 | cxx_flags = "-Wno-deprecated-register" 83 | when /clang version (\d+\.\d+)\.\d/ 84 | version = Float($1) 85 | if version < 3.5 86 | std = "gnu++11" 87 | elsif version < 5 88 | std = "gnu++14" 89 | else 90 | std = "gnu++17" 91 | end 92 | end 93 | 94 | if std 95 | @cxx_flags += " -std=#{std}" 96 | @cxx_flags += " #{cxx_flags}" if cxx_flags 97 | [version, " (#{std})"] 98 | else 99 | [version, ""] 100 | end 101 | end 102 | end 103 | 104 | def try_cxx_warning_flag(warning_flag) 105 | conftest_cxx = "#{CONFTEST}.cpp" 106 | begin 107 | source = "int main(void) {return 0;}" 108 | open(conftest_cxx, "wb") do |cxx_file| 109 | cxx_file.print(source) 110 | end 111 | flags = "-Werror #{warning_flag}" 112 | xsystem(RbConfig.expand("$(CXX) #{flags} -c #{conftest_cxx}")) 113 | ensure 114 | log_src(source) 115 | MakeMakefile.rm_f(conftest_cxx) 116 | end 117 | end 118 | 119 | def check_warning_flags 120 | flags = [] 121 | warning_flags = [] 122 | Shellwords.split(@cxx_flags).each do |flag| 123 | if flag.start_with?("-W") 124 | warning_flags << flag 125 | else 126 | flags << flag 127 | end 128 | end 129 | warning_flags.each do |warning_flag| 130 | if try_cxx_warning_flag(warning_flag.gsub(/\A-Wno-/, "-W")) 131 | flags << warning_flag 132 | end 133 | end 134 | case RUBY_PLATFORM 135 | when /windows/, /mingw/ 136 | @cxx_flags = flags.join(" ") 137 | else 138 | @cxx_flags = Shellwords.join(flags) 139 | end 140 | end 141 | end 142 | end 143 | -------------------------------------------------------------------------------- /include/ruby/object.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | 7 | namespace rb { 8 | class Object { 9 | public: 10 | explicit Object(VALUE rb_object=Qnil) : 11 | rb_object_(rb_object), 12 | is_gc_guarding_(false) { 13 | } 14 | 15 | explicit Object(const char *name) : 16 | rb_object_(rb_funcall(rb_cObject, 17 | rb_intern("const_get"), 18 | 1, 19 | rb_str_new_static(name, strlen(name)))), 20 | is_gc_guarding_(false) { 21 | } 22 | 23 | Object(Object const &object) : 24 | rb_object_(object), 25 | is_gc_guarding_(false) { 26 | } 27 | 28 | virtual ~Object() { 29 | if (is_gc_guarding_) { 30 | rb_gc_unregister_address(&rb_object_); 31 | } 32 | } 33 | 34 | inline explicit operator bool() const { 35 | return RTEST(rb_object_); 36 | } 37 | 38 | inline VALUE to_ruby() const { 39 | return rb_object_; 40 | } 41 | 42 | inline operator VALUE() const { 43 | return rb_object_; 44 | } 45 | 46 | inline bool is_nil() const { 47 | return NIL_P(rb_object_); 48 | } 49 | 50 | inline void guard_from_gc() { 51 | if (!is_gc_guarding_) { 52 | return; 53 | } 54 | 55 | is_gc_guarding_ = true; 56 | rb_gc_register_address(&rb_object_); 57 | } 58 | 59 | inline Object send(ID name_id) { 60 | VALUE rb_result = rb_funcall(rb_object_, name_id, 0); 61 | return Object(rb_result); 62 | } 63 | 64 | inline Object send(const char *name) { 65 | return send(rb_intern(name)); 66 | } 67 | 68 | inline Object send(Object name) { 69 | return send(rb_intern_str(name)); 70 | } 71 | 72 | inline Object send(ID name_id, std::initializer_list args) { 73 | auto n = args.size(); 74 | VALUE rb_args[n]; 75 | int i = 0; 76 | for (auto arg : args) { 77 | rb_args[i++] = arg; 78 | } 79 | VALUE rb_result = rb_funcallv(rb_object_, 80 | name_id, 81 | static_cast(n), 82 | rb_args); 83 | return Object(rb_result); 84 | } 85 | 86 | inline Object send(const char *name, std::initializer_list args) { 87 | return send(rb_intern(name), args); 88 | } 89 | 90 | inline Object send(Object name, std::initializer_list args) { 91 | return send(rb_intern_str(name), args); 92 | } 93 | 94 | inline Object send(ID name_id, 95 | std::initializer_list args, 96 | MethodWithoutArguments block) { 97 | auto n = args.size(); 98 | VALUE rb_args[n]; 99 | int i = 0; 100 | for (auto arg : args) { 101 | rb_args[i++] = arg; 102 | } 103 | auto call_block = [](RB_BLOCK_CALL_FUNC_ARGLIST(rb_data, rb_block)) { 104 | auto block = reinterpret_cast(rb_block); 105 | return block(rb_data); 106 | }; 107 | #ifdef RUBY_BACKWARD_CXXANYARGS_HPP 108 | # define CAST_CALLER(caller) caller 109 | #else 110 | # define CAST_CALLER(caller) reinterpret_cast(+caller) 111 | #endif 112 | auto rb_result = rb_block_call(rb_object_, 113 | name_id, 114 | static_cast(n), 115 | rb_args, 116 | CAST_CALLER(call_block), 117 | reinterpret_cast(block)); 118 | #undef CAST_CALLER 119 | return Object(rb_result); 120 | } 121 | 122 | inline Object send(const char *name, 123 | std::initializer_list args, 124 | MethodWithoutArguments block) { 125 | return send(rb_intern(name), args, block); 126 | } 127 | 128 | inline Object send(Object name, 129 | std::initializer_list args, 130 | MethodWithoutArguments block) { 131 | return send(rb_intern_str(name), args, block); 132 | } 133 | 134 | inline Object ivar_set(ID name_id, VALUE value) { 135 | return Object(rb_ivar_set(rb_object_, name_id, value)); 136 | } 137 | 138 | inline Object ivar_set(const char *name, VALUE value) { 139 | return ivar_set(rb_intern(name), value); 140 | } 141 | 142 | inline Object ivar_set(Object name, VALUE value) { 143 | return ivar_set(rb_intern_str(name), value); 144 | } 145 | 146 | inline Object ivar_get(ID name_id) { 147 | return Object(rb_ivar_get(rb_object_, name_id)); 148 | } 149 | 150 | inline Object ivar_get(const char *name) { 151 | return ivar_get(rb_intern(name)); 152 | } 153 | 154 | inline Object ivar_get(Object name) { 155 | return ivar_get(rb_intern_str(name)); 156 | } 157 | 158 | private: 159 | VALUE rb_object_; 160 | bool is_gc_guarding_; 161 | }; 162 | } 163 | -------------------------------------------------------------------------------- /include/ruby/class.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | namespace rb { 11 | using MethodTable = std::unordered_map; 12 | 13 | struct MethodDefinition { 14 | MethodDefinition(std::string name_, Function *function_) : 15 | name(name_), 16 | function(function_) { 17 | } 18 | 19 | std::string name; 20 | Function *function; 21 | }; 22 | using MethodDefinitions = std::vector; 23 | 24 | namespace { 25 | inline MethodTable *method_table_from_ruby(VALUE rb_method_table) { 26 | return reinterpret_cast(NUM2ULL(rb_method_table)); 27 | } 28 | 29 | inline VALUE method_table_to_ruby(MethodTable *method_table) { 30 | return ULL2NUM(reinterpret_cast(method_table)); 31 | } 32 | 33 | inline MethodDefinitions * 34 | method_definitions_from_ruby(VALUE rb_definitions) { 35 | return reinterpret_cast(NUM2ULL(rb_definitions)); 36 | } 37 | 38 | inline VALUE method_definitions_to_ruby(MethodDefinitions *definitions) { 39 | return ULL2NUM(reinterpret_cast(definitions)); 40 | } 41 | 42 | inline VALUE call_func(int argc, VALUE *argv, VALUE self) { 43 | auto rb_method_table = 44 | rb_ivar_get(rb_obj_class(self), rb_intern("__method_table__")); 45 | auto method_table = method_table_from_ruby(rb_method_table); 46 | auto method_name_symbol = rb_funcall(self, rb_intern("__method__"), 0); 47 | auto function = (*method_table)[rb_sym2id(method_name_symbol)]; 48 | return function->call(self, argc, argv); 49 | } 50 | 51 | inline bool flush_method_definitions(VALUE klass) { 52 | ID id_method_definitions = rb_intern("__method_definitions__"); 53 | auto rb_definitions = rb_ivar_get(klass, id_method_definitions); 54 | if (NIL_P(rb_definitions)) { 55 | return false; 56 | } 57 | 58 | auto definitions = method_definitions_from_ruby(rb_definitions); 59 | auto rb_method_table = rb_ivar_get(klass, rb_intern("__method_table__")); 60 | auto method_table = method_table_from_ruby(rb_method_table); 61 | for (const auto &definition : *definitions) { 62 | ID name_id = rb_intern(definition.name.c_str()); 63 | (*method_table)[name_id] = definition.function; 64 | rb_define_method(klass, 65 | definition.name.c_str(), 66 | reinterpret_cast(call_func), 67 | -1); 68 | } 69 | rb_ivar_set(klass, id_method_definitions, Qnil); 70 | return true; 71 | } 72 | 73 | inline VALUE method_missing(int argc, VALUE *argv, VALUE self) { 74 | auto klass = rb_obj_class(self); 75 | 76 | if (flush_method_definitions(klass)) { 77 | auto rb_method_table = rb_ivar_get(klass, rb_intern("__method_table__")); 78 | auto method_table = method_table_from_ruby(rb_method_table); 79 | 80 | VALUE rb_name_symbol; 81 | VALUE rb_args; 82 | rb_scan_args(argc, argv, "1*", &rb_name_symbol, &rb_args); 83 | auto function = (*method_table)[rb_sym2id(rb_name_symbol)]; 84 | if (function) { 85 | return function->call(self, 86 | static_cast(RARRAY_LEN(rb_args)), 87 | RARRAY_PTR(rb_args)); 88 | } 89 | } 90 | 91 | return rb_call_super(argc, argv); 92 | } 93 | 94 | inline VALUE respond_to_missing_p(VALUE self, 95 | VALUE rb_name_symbol, 96 | VALUE rb_include_private) { 97 | auto klass = rb_obj_class(self); 98 | 99 | if (flush_method_definitions(klass)) { 100 | auto rb_method_table = rb_ivar_get(klass, rb_intern("__method_table__")); 101 | auto method_table = method_table_from_ruby(rb_method_table); 102 | 103 | auto function = (*method_table)[rb_sym2id(rb_name_symbol)]; 104 | if (function) { 105 | return Qtrue; 106 | } 107 | } 108 | 109 | VALUE rb_args[] = {rb_name_symbol, rb_include_private}; 110 | return rb_call_super(2, rb_args); 111 | } 112 | } 113 | 114 | class Class: public Object { 115 | public: 116 | Class(const char *name) : 117 | Class(RTEST(rb_funcall(rb_cObject, 118 | rb_intern("const_defined?"), 119 | 1, 120 | rb_str_new_static(name, strlen(name)))) ? 121 | rb_funcall(rb_cObject, 122 | rb_intern("const_get"), 123 | 1, 124 | rb_str_new_static(name, strlen(name))) : 125 | rb_define_class(name, rb_cObject)) { 126 | } 127 | 128 | Class(const char *name, VALUE parent) : 129 | Class(rb_define_class(name, parent)) { 130 | } 131 | 132 | Class(VALUE klass) : 133 | Object(klass), 134 | class_(klass), 135 | method_table_(new MethodTable()), 136 | lazy_define_method_(false), 137 | method_definitions_(nullptr) { 138 | rb_iv_set(class_, 139 | "__method_table__", 140 | method_table_to_ruby(method_table_)); 141 | rb_iv_set(class_, "__method_definitions__", Qnil); 142 | } 143 | 144 | inline Class &define_method(const char *name, 145 | MethodWithoutArguments body) { 146 | auto function = new FunctionWithoutArgument(body); 147 | return define_method(name, function); 148 | } 149 | 150 | inline Class &define_method(const char *name, 151 | MethodWithArguments body) { 152 | auto function = new FunctionWithArguments(body); 153 | return define_method(name, function); 154 | } 155 | 156 | inline Class &define_method(const char *name, 157 | MethodWithArgumentsCompatible body) { 158 | auto function = new FunctionWithArgumentsCompatible(body); 159 | return define_method(name, function); 160 | } 161 | 162 | inline Class &define_method(const char *name, Function *function) { 163 | if (lazy_define_method_) { 164 | method_definitions_->emplace_back(name, function); 165 | } else { 166 | ID name_id = rb_intern(name); 167 | (*method_table_)[name_id] = function; 168 | rb_define_method(class_, 169 | name, 170 | reinterpret_cast(call_func), 171 | -1); 172 | } 173 | return (Class &)*this; 174 | } 175 | 176 | inline Class &enable_lazy_define_method() { 177 | if (lazy_define_method_) { 178 | return (Class &)*this; 179 | } 180 | 181 | lazy_define_method_ = true; 182 | method_definitions_ = new MethodDefinitions(); 183 | rb_iv_set(class_, 184 | "__method_definitions__", 185 | method_definitions_to_ruby(method_definitions_)); 186 | rb_define_method(class_, 187 | "method_missing", 188 | reinterpret_cast(method_missing), 189 | -1); 190 | rb_define_method(class_, 191 | "respond_to_missing?", 192 | reinterpret_cast(respond_to_missing_p), 193 | -1); 194 | return (Class &)*this; 195 | } 196 | 197 | private: 198 | VALUE class_; 199 | MethodTable *method_table_; 200 | bool lazy_define_method_; 201 | MethodDefinitions *method_definitions_; 202 | }; 203 | } 204 | --------------------------------------------------------------------------------