[Tutorial] [Ruby] Creating and debugging C native extension gem

English

A tutorial on how to create a Ruby gem with C native extensions and how to debug with gdb.

The code used in this article has been uploaded to GitHub.

Build CRuby

This step is not mandatory, so you can skip it.

By building ruby, Ruby itself can be stepped during gem debug development, which helps gem development.

Here, use rbenv + ruby-build. Suppose rbenv is installed in ~ / .rbenv.

# --keep keeps source code in ~/.rbenv/sources/2.6.1/ruby-2.6.1/
rbenv install --keep --verbose 2.6.1
rbenv shell 2.6.1

# check that ruby is debuggable
type ruby           # => ruby is /home/wsh/.rbenv/shims/ruby
rbenv which ruby    # => /home/wsh/.rbenv/versions/2.6.1/bin/ruby
gdb -q ~/.rbenv/versions/2.6.1/bin/ruby
# (gdb) break main
# (gdb) run
# Breakpoint 1, main (argc=1, argv=0x7fffffffdd58) at ./main.c:30
# 30  {
# (gdb) list
# 25  #include <stdlib.h>
# 26  #endif
# 27  
# 28  int
# 29  main(int argc, char **argv)
# 30  {
# 31  #ifdef RUBY_DEBUG_ENV
# 32      ruby_set_debug_option(getenv("RUBY_DEBUG"));
# 33  #endif
# 34  #ifdef HAVE_LOCALE_H

On macOS [codesign problem](https://www.google.com/search?safe=off&ei=ijNmXN3jB8aq8QXvvpfgDA&q=please+check+gdb+is+codesigned+-+see+taskgated%288%29&oq=please+check+gdb+ is + codesigned +-+ see + taskgated% 288% 29 & gs_l = psy-ab.3..0.232958.232958..233371 ... 0.0..0.92.92.1 ...... 0 .... 2j1..gws -wiz ....... 0i71.1sEziP0mz_E), so use lldb instead of gdb or GDB Wiki Description ) Follow code signing.

Disable optimization (-O0) for easier debugging:

git clone https://github.com/ruby/ruby.git
cd ruby/
autoconf -v  # as written in README.md
mkdir build; cd build/
# --disable-install-doc saves build time
../configure --prefix=$HOME/.rbenv/versions/trunk --disable-install-doc --enable-debug-env optflags="-O0"
make V=1 -j4
make install
rbenv shell trunk
ruby --version  # 2.?.?dev
gdb -q ~/.rbenv/versions/trunk/bin/ruby

or:

wget https://cache.ruby-lang.org/pub/ruby/2.6/ruby-2.6.1.tar.gz
tar xvf ruby-2.6.1.tar.gz
cd ruby-2.6.1/
mkdir build/; cd build/
../configure --prefix=$HOME/.rbenv/versions/trunk --disable-install-doc --enable-debug-env optflags="-O0"
make V=1 -j4
make install
rbenv shell 2.6.1-dbg
ruby --version  # 2.6.1
gdb -q ~/.rbenv/versions/2.6.1-dbg/bin/ruby

-debug = flags = -ggdb3 is the default, so you don't need to specify debug options. --MacOS /usr/lib/libcrypto.dylib is too old to build the OpenSSL module, so after brew install openssl, add CPPFLAGS ="-I / usr / local / opt / openssl to configure. Add / include "LDFL AGS ="-L / usr / local / opt / openssl / lib ". -[Display build-time commands with V = 1`](https://www.gnu.org/software/automake/manual/html_node/Automake-Silent-Rules.html).

From now on, I will use rbenv shell trunk (~ / .rbenv / versions / trunk / ).

Create and build gem

rbenv shell trunk
cd ~/work/
bundle gem example_ext --coc --ext --mit --test
cd example_ext/
bin/setup  # as described in ./README.md

An error will occur as shown below.

$ bin/setup

bundle install
+ bundle install
You have one or more invalid gemspecs that need to be fixed.
The gemspec at /home/wsh/work/example_ext/example_ext.gemspec is not valid. Please fix this gemspec.
The validation error was 'metadata['homepage_uri'] has invalid link: "TODO: Put your gem's website or public repo URL here."'

Edit and fix ʻexample_ext.gemspec`:

diff --git a/example_ext.gemspec b/example_ext.gemspec
index 4b9d3c1..1446707 100644
--- a/example_ext.gemspec
+++ b/example_ext.gemspec
@@ -9,9 +9,9 @@ Gem::Specification.new do |spec|
   spec.authors       = ["Wataru Ashihara"]
   spec.email         = ["[email protected]"]
 
-  spec.summary       = %q{TODO: Write a short summary, because RubyGems requires one.}
-  spec.description   = %q{TODO: Write a longer description or delete this line.}
-  spec.homepage      = "TODO: Put your gem's website or public repo URL here."
+  spec.summary       = %q{Write a short summary, because RubyGems requires one.}
+  spec.description   = %q{Write a longer description or delete this line.}
+  # spec.homepage      = "TODO: Put your gem's website or public repo URL here."
   spec.license       = "MIT"
 
   # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
@@ -19,9 +19,9 @@ Gem::Specification.new do |spec|
   if spec.respond_to?(:metadata)
     spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
 
-    spec.metadata["homepage_uri"] = spec.homepage
-    spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here."
-    spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
+    # spec.metadata["homepage_uri"] = spec.homepage
+    # spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here."
+    # spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
   else
     raise "RubyGems 2.0 or newer is required to protect against " \
       "public gem pushes."

View on GitHub

This should pass the build.

bin/setup
bundle exec rake install  # as described in README.md
bundle exec ruby -e 'require "example_ext"; p ExampleExt::VERSION'
# => "0.1.0"

Let's implement a C extension.

diff --git a/ext/example_ext/example_ext.c b/ext/example_ext/example_ext.c
index c89d90a..f47c72e 100644
--- a/ext/example_ext/example_ext.c
+++ b/ext/example_ext/example_ext.c
@@ -2,8 +2,18 @@
 
 VALUE rb_mExampleExt;
 
+static VALUE
+example_hello(int argc, VALUE *argv)
+{
+  printf("hello\n");
+
+  return Qnil;
+}
+
 void
 Init_example_ext(void)
 {
   rb_mExampleExt = rb_define_module("ExampleExt");
+
+  rb_define_module_function(rb_mExampleExt, "hello", example_hello, -1);
 }

View on GitHub

bin/setup
bundle exec rake install
bundle exec ruby -e 'require "example_ext"; ExampleExt::hello'
# => hello

The Ruby C API is rarely documented. The Definitive Guide to Ruby's C API (http://silverhammermba.github.io/emberb/c/) is recommended as the first guide to read.

Debugging C extensions with gdb

Build the C extension with optimization disabled (-O0) and with debug symbols ( -ggdb3).

cd ext/example_ext/
vim extconf.rb
ruby extconf.rb # specify -ggdb3 -O0
make V=1        # check -ggdb3 -O0
make clean
cd ../../
bundle exec rake install
diff --git a/ext/example_ext/extconf.rb b/ext/example_ext/extconf.rb
index f657c82..2ca74f1 100644
--- a/ext/example_ext/extconf.rb
+++ b/ext/example_ext/extconf.rb
@@ -1,3 +1,6 @@
 require "mkmf"
 
+CONFIG["debugflags"] = "-ggdb3"
+CONFIG["optflags"] = "-O0"
+
 create_makefile("example_ext/example_ext")

View on GitHub

$ make V=1
gcc -I. ... -O0 -ggdb3 ... -o example_ext.o -c example_ext.c
...

COINFIG is equivalent to RbConfig :: MAKEFILE_CONFIG, which stores the build config for ruby. So the above diff may not be needed, but make sure that make V = 1 is -O0 -ggdb3.

In bear and intercept-build [compilation database (compile_commands.json)]( When creating (https://clang.llvm.org/docs/JSONCompilationDatabase.html), you can do static analysis with the following.

bundle exec rake clean && bundle exec bear rake build
jq '.' compile_commands.json > compile_commands.json.orig
jq --arg IPWD "-I$PWD" --arg IRUBY \
  "-I$HOME/src/ruby/include" '.[].arguments |= [ .[0], $IPWD, $IRUBY, .[1:][] ]' \
  compile_commands.json.orig > compile_commands.json

Try debugging with gdb.

# (a) recommended:
bundle exec \
  gdb -q -ex 'set breakpoint pending on' -ex 'b example_hello' -ex run --args ruby -e 'require "example_ext"; ExampleExt::hello'
# (b) or:
env RUBYLIB=./lib \
  gdb -q -ex 'set breakpoint pending on' -ex 'b example_hello' -ex run --args ~/.rbenv/versions/trunk/bin/ruby -e 'require "example_ext"; ExampleExt::hello'
# (c) unrecommended:
gdb -q -ex 'set breakpoint pending on' -ex 'b example_hello' -ex run --args ~/.rbenv/versions/trunk/bin/ruby -e 'require "example_ext"; ExampleExt::hello'

TL; DR:

(a): In bundle exec,require "example_ext"loads (indirectly) /path/to/example_ext/example_ext.so (ʻexample_ext.bundle on macOS). ʻExample_ext.so debug information refers to /path/to/example_ext/ext/example_ext/example_ext.c. In (b) you need to specify the location of the ruby binary directly. This is because without bundle exec, the ruby command points to a shell script and cannot be read by gdb. You also need to specify RUBY_LIB = ./ lib. Without it (c), the shared library loaded would be ~ / .rbenv / versions / trunk /.../ example_ext.so and debug .rbenv / versions / trunk /.../ example_ext.c. It will be done [^ 1]. You can still reference the original source at gdb set substitute-path.

For more details, please execute the following command.

echo $PATH
which -a ruby
bundle exec echo $PATH
bundle exec which -a ruby
file ~/.rbenv/shims/ruby
file ~/.rbenv/versions/trunk/bin/ruby
ruby -e 'p $:'  # or $LOAD_PATH
bundle exec ruby -e 'p $:'
env RUBYLIB=./lib ruby -e 'p $:'

reference

license

<img alt = "Creative Commons License" style = "border-width: 0" src = "https" //i.creativecommons.org/l/by/4.0/88x31.png "/>
This work is Creative Commons Attribution 4.0 International Licenses .

For maintainers of official documentation for related software: If you would like to include the content of this article in the official documentation, please comment on this article or twitter @wata_ash ) Please contact us.

[^ 1]: The rake install task runs the build task, which runs gem install /path/to/example_ext/pkg/example_ext-0.1.0.gem. gem install example_ext-0.1.0.gem expands the C source to~ / .rbenv / versions / trunk / lib / ruby / gems / 2.7.0 / gems / example_ext-0.1.0 /and then compiles Therefore, the source path of the debug information will be ~ / .rbenv / versions / trunk / lib / ruby / gems / 2.7.0 / gems / example_ext-0.1.0 / ext / example_ext / example_ext.c. I will end up. This is not documented, but the source (1) (2) [(3)](https://github.com/rubygems/rubygems/blob/v3. 0.2 / lib / rubygems / installer.rb # L318) (4) And bundle exec ruby -rtracer ~ / .rbenv / versions / trunk / bin / rake --trace install.

Recommended Posts

[Tutorial] [Ruby] Creating and debugging C native extension gem
Ruby C extension and volatile
Ruby and Gem
Created a native extension of Ruby with Rust and published it as a gem
I implemented Ruby with Ruby (and C) (I played with builtin)
I made a Ruby extension library in C
Create a native extension of Ruby in Rust
Create a native extension of Ruby in Rust
Created a native extension of Ruby with Rust and published it as a gem