This article explains how to create a minimum Ruby gem with native extension written in C and debug it with gdb.
I uploaded complete code to GitHub.
Tested with:
- Ubuntu 20.04 (with self-built Linux kernel 5.11.22+)
- Ruby 2.7.4
Warning
Using Ruby version 2.7.4 is strongly recommended at the first time of doing this tutorial.
I think making Ruby-C-extensions has a high probability of encountering errors –
for instance, as of 2021-08-14, with Ruby 3.0.2, bundle exec rake install
fails with the following error:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
$ bundle exec rake install
install -c tmp/x86_64-linux/example_ext/3.0.2/example_ext.so lib/example_ext/example_ext.so
cp tmp/x86_64-linux/example_ext/3.0.2/example_ext.so tmp/x86_64-linux/stage/lib/example_ext/example_ext.so
example_ext 0.1.0 built to pkg/example_ext-0.1.0.gem.
rake aborted!
Couldn't install gem, run `gem install /tmp/ruby-c-extension/example_ext/pkg/example_ext-0.1.0.gem' for more detailed output
/home/wsh/.rbenv/versions/3.0.2-dbg/bin/bundle:23:in `load'
/home/wsh/.rbenv/versions/3.0.2-dbg/bin/bundle:23:in `<main>'
Tasks: TOP => install
(See full trace by running task with --trace)
$ bundle exec gem install /tmp/ruby-c-extension/example_ext/pkg/example_ext-0.1.0.gem
ERROR: Error installing /tmp/ruby-c-extension/example_ext/pkg/example_ext-0.1.0.gem:
ERROR: Failed to build gem native extension.
No such file or directory @ dir_s_mkdir - /home/wsh/.rbenv/versions/3.0.2-dbg/lib/ruby/gems/3.0.0/gems/example_ext-0.1.0/ext/example_ext/.gem.20210813-500914-tvlpga
Gem files will remain installed in /home/wsh/.rbenv/versions/3.0.2-dbg/lib/ruby/gems/3.0.0/gems/example_ext-0.1.0 for inspection.
Results logged to /home/wsh/.rbenv/versions/3.0.2-dbg/lib/ruby/gems/3.0.0/extensions/x86_64-linux/3.0.0-static/example_ext-0.1.0/gem_make.out
|
So please use Ruby 2.7.4 at first, to avoid such errors.
Build CRuby (optional)
By this step, we’ll be able to debug (step into) ruby
C source with gdb.
It’s not necessary but would be greatly help when debugging gems.
Here we use rbenv with ruby-build plugin.
Here rbenv is assumed to be installed in ~/.rbenv
.
1
2
3
4
5
6
7
|
# --keep keeps source code in ~/.rbenv/sources/2.7.4/ruby-2.7.4/
rbenv install --keep --verbose 2.7.4
rbenv shell 2.7.4
# confirm that ruby is debuggable
rbenv which ruby # => /home/wsh/.rbenv/versions/2.7.4/bin/ruby
gdb -q ~/.rbenv/versions/2.7.4/bin/ruby
|
In the gdb session, execute the following commands:
break main
run -e 'puts("hello")'
info line
info source
list
continue
quit
The output should be like:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
(gdb) break main
Breakpoint 1 at 0x1120: file ./main.c, line 38.
(gdb) run -e 'puts("hello")'
Starting program: /home/wsh/.rbenv/versions/2.7.4/bin/ruby -e 'puts("hello")'
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Breakpoint 1, main (argc=3, argv=0x7fffffffd748) at ./main.c:38
38 {
(gdb) info line
Line 38 of "./main.c" starts at address 0x555555555120 <main> and ends at 0x555555555149 <main+41>.
(gdb) info source
Current source file is ./main.c
Compilation directory is /home/wsh/.rbenv/sources/2.7.4/ruby-2.7.4
Located in /home/wsh/.rbenv/sources/2.7.4/ruby-2.7.4/main.c
Contains 52 lines.
Source language is c.
Producer is GNU C99 9.3.0 -mtune=generic -march=x86-64 -ggdb3 -O3 -std=gnu99 -fPIC -fstack-protector-strong -fno-strict-overflow -fvisibility=hidden -fexcess-precision=standard -fasynchronous-unwind-tables -fstack-protector-strong -fstack-clash-protection -fcf-protection.
Compiled with DWARF 2 debugging format.
Includes preprocessor macro info.
(gdb) list
33 #include <stdlib.h>
34 #endif
35
36 int
37 main(int argc, char **argv)
38 {
39 #ifdef RUBY_DEBUG_ENV
40 ruby_set_debug_option(getenv("RUBY_DEBUG"));
41 #endif
42 #ifdef HAVE_LOCALE_H
(gdb) continue
Continuing.
hello
[Inferior 1 (process 360063) exited normally]
(gdb) quit
|
Ruby without optimization (-O0
) is more debuggable:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
git clone https://github.com/ruby/ruby.git
git checkout v2_7_4
cd ruby/
autoconf # as written in README.md
mkdir build; cd build/
# --disable-install-doc saves build time
../configure --prefix=$HOME/.rbenv/versions/2.7.4-dbg --disable-install-doc --enable-debug-env optflags="-O0"
make V=1 -j4
make install
rbenv shell 2.7.4-dbg
rbenv version # 2.7.4-dbg (set by RBENV_VERSION environment variable)
ruby --version # ruby 2.7.4p191 (2021-07-07 revision a21a3b7d23) [x86_64-linux]
rbenv which ruby # /home/wsh/.rbenv/versions/2.7.4-dbg/bin/ruby
gdb -q ~/.rbenv/versions/2.7.4-dbg/bin/ruby -ex 'b main' -ex 'r -e "puts 42"' -ex 'c' -ex 'q' # Breakpoint 1, main (argc=3, argv=0x7fffffffd728) at ../main.c:38
|
or you can use the tar ball:
1
2
3
4
5
6
7
8
9
10
11
12
|
wget https://cache.ruby-lang.org/pub/ruby/2.7/ruby-2.7.4.tar.gz
tar xvf ruby-2.7.4.tar.gz
cd ruby-2.7.4/
mkdir build/; cd build/
# --disable-install-doc saves build time
../configure --prefix=$HOME/.rbenv/versions/2.7.4-dbg --disable-install-doc --enable-debug-env optflags="-O0"
make V=1 -j4
make install
rbenv shell 2.7.4-dbg
rbenv version # 2.7.4-dbg (set by RBENV_VERSION environment variable)
ruby --version # ruby 2.7.4p107 (2021-07-07 revision a21a3b7d23) [x86_64-linux]
gdb -q ~/.rbenv/versions/2.7.4-dbg/bin/ruby -ex 'b main' -ex 'r -e "puts 42"' -ex 'c' -ex 'q' # Breakpoint 1, main (argc=3, argv=0x7fffffffd728) at ../main.c:38
|
In the following sections, rbenv shell 2.7.4-dbg
(~/.rbenv/versions/2.7.4-dbg/
) is supposed to be used.
Create and build gem
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
rbenv shell 2.7.4-dbg
which bundle # /home/wsh/.rbenv/shims/bundle
rbenv which bundle # /home/wsh/.rbenv/versions/2.7.4-dbg/bin/bundle
bundle --version # Bundler version 2.1.4
# Enter, Enter, Enter...
# Do you want to generate tests with your gem? -> none
# Do you want to license your code permissively under the MIT license? -> n
# Do you want to include a code of conduct in gems you generate? -> n
# Or, of course you can select y, y, y...
# These selections will be stored into ~/.bundle/config .
bundle gem example_ext --ext
cd example_ext/
git commit -m 'bundle gem example_ext --ext'
bin/setup # as described in ./README.md
|
bin/setup
fails with the following error:
1
2
3
4
5
6
7
|
$ bin/setup
bundle install
+ bundle install
You have one or more invalid gemspecs that need to be fixed.
The gemspec at /tmp/ruby-c-extension/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."'
|
To fix it, edit example_ext.gemspec
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
diff --git a/example_ext.gemspec b/example_ext.gemspec
index f190b0e..a34275f 100644
--- a/example_ext.gemspec
+++ b/example_ext.gemspec
@@ -6,16 +6,16 @@ Gem::Specification.new do |spec|
spec.authors = ["Wataru Ashihara"]
spec.email = ["wataash@wataash.com"]
- 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{TODO: Write a longer description or delete this line.}
+ # spec.homepage = "TODO: Put your gem's website or public repo URL here."
spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
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."
# Specify which files should be added to the gem when it is released.
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
View on GitHub
Now the building should succeed:
1
2
3
|
bin/setup
bundle exec rake install # as described in README.md
bundle exec ruby -e 'require "example_ext"; p ExampleExt::VERSION' # "0.1.0"
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
$ bin/setup
bundle install
+ bundle install
Using rake 12.3.3
Using bundler 2.1.4
Using example_ext 0.1.0 from source at `.`
Using rake-compiler 1.1.1
Bundle complete! 3 Gemfile dependencies, 4 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.
# Do any other automated setup that you need to do here
$ bundle exec rake install
mkdir -p tmp/x86_64-linux/example_ext/2.7.4
cd tmp/x86_64-linux/example_ext/2.7.4
/home/wsh/.rbenv/versions/2.7.4-dbg/bin/ruby -I. ../../../../ext/example_ext/extconf.rb
creating Makefile
cd -
cd tmp/x86_64-linux/example_ext/2.7.4
/usr/bin/make
compiling ../../../../ext/example_ext/example_ext.c
linking shared-object example_ext/example_ext.so
cd -
mkdir -p tmp/x86_64-linux/stage/lib/example_ext
install -c tmp/x86_64-linux/example_ext/2.7.4/example_ext.so lib/example_ext/example_ext.so
cp tmp/x86_64-linux/example_ext/2.7.4/example_ext.so tmp/x86_64-linux/stage/lib/example_ext/example_ext.so
example_ext 0.1.0 built to pkg/example_ext-0.1.0.gem.
example_ext (0.1.0) installed.
$ bundle exec ruby -e 'require "example_ext"; p ExampleExt::VERSION'
"0.1.0"
|
Let’s implement and execute the C extension. Here we define the module method ExampleExt.hello
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
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(VALUE obj)
+{
+ 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, 0);
}
|
View on GitHub
1
2
|
bundle exec rake install
bundle exec ruby -e 'require "example_ext"; ExampleExt.hello' # hello
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
$ bundle exec rake install
cd tmp/x86_64-linux/example_ext/2.7.4
/usr/bin/make
compiling ../../../../ext/example_ext/example_ext.c
linking shared-object example_ext/example_ext.so
cd -
install -c tmp/x86_64-linux/example_ext/2.7.4/example_ext.so lib/example_ext/example_ext.so
cp tmp/x86_64-linux/example_ext/2.7.4/example_ext.so tmp/x86_64-linux/stage/lib/example_ext/example_ext.so
example_ext 0.1.0 built to pkg/example_ext-0.1.0.gem.
example_ext (0.1.0) installed.
$ bundle exec ruby -e 'require "example_ext"; ExampleExt.hello'
hello
|
The Definitive Guide to Ruby’s C API is a great guide to get started with the C API.
Debug native extension with gdb
Build the native extension without optimization (-O0
) and with debug symbols (-ggdb3
).
1
2
3
4
5
6
7
|
cd ext/example_ext/
vim extconf.rb # apply diff below
ruby extconf.rb
make clean && make V=1 # gcc -ggdb3 -O0
make clean
cd ../../
bundle exec rake install
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
diff --git a/ext/example_ext/extconf.rb b/ext/example_ext/extconf.rb
index f657c82..7f21db4 100644
--- a/ext/example_ext/extconf.rb
+++ b/ext/example_ext/extconf.rb
@@ -1,3 +1,13 @@
+require "rbconfig"
+if RbConfig::MAKEFILE_CONFIG["CFLAGS"].include?("-g -O2")
+ fixed_CFLAGS = RbConfig::MAKEFILE_CONFIG["CFLAGS"].sub("-g -O2", "$(cflags)")
+ puts("Fix CFLAGS: #{RbConfig::MAKEFILE_CONFIG["CFLAGS"]} -> #{fixed_CFLAGS}")
+ RbConfig::MAKEFILE_CONFIG["CFLAGS"] = fixed_CFLAGS
+end
+
require "mkmf"
+CONFIG["optflags"] = "-O0"
+CONFIG["debugflags"] = "-ggdb3"
+
create_makefile("example_ext/example_ext")
|
Detailed explanation of this diff
CONFIG["optflags"] = "-O0"
and CONFIG["debugflags"] = "-ggdb3"
changes the Makefile variable optflags
and debugflags
, which are used as CFLAGS
.
1
2
3
4
|
cflags = $(optflags) $(debugflags) $(warnflags)
optflags = -O0
debugflags = -ggdb3
CFLAGS = $(CCDLFLAGS) $(cflags) $(ARCH_FLAG)
|
If you built Ruby with optflags="-O0"
(and defaultly debugflags="-ggdb3"
),
these substitutions are not needed, since they are set to the ones with which Ruby built,
which are defined in
~/.rbenv/versions/2.7.4-dbg/lib/ruby/2.7.0/x86_64-linux/rbconfig.rb
:
1
2
|
CONFIG["debugflags"] = "-ggdb3"
CONFIG["optflags"] = "-O0"
|
CFLAGS
in rbconfig.rb
is like this:
1
2
3
4
5
6
7
8
|
# 2.7.1: ~/.rbenv/versions/2.7.1/lib/ruby/2.7.0/x86_64-linux/rbconfig.rb
CONFIG["CFLAGS"] = "$(cflags) -fPIC"
# 2.7.4: ~/.rbenv/versions/2.7.4/lib/ruby/2.7.0/x86_64-linux/rbconfig.rb
CONFIG["CFLAGS"] = "-g -O2 -fPIC"
# 2.7.4-dbg: ~/.rbenv/versions/2.7.4-dbg/lib/ruby/2.7.0/x86_64-linux/rbconfig.rb
CONFIG["CFLAGS"] = "-g -O2"
# 3.0.2: ~/.rbenv/versions/3.0.2/lib/ruby/3.0.0/x86_64-linux/rbconfig.rb
CONFIG["CFLAGS"] = "$(cflags) -fPIC"
|
And Makefile
s generated by ruby extconf.rb
are like this:
1
2
3
4
5
6
7
8
|
# 2.7.1
CFLAGS = $(CCDLFLAGS) $(cflags) -fPIC $(ARCH_FLAG)
# 2.7.4
CFLAGS = $(CCDLFLAGS) -g -O2 -fPIC $(ARCH_FLAG)
# 2.7.4-dbg
CFLAGS = $(CCDLFLAGS) -g -O2 $(ARCH_FLAG)
# 3.0.2
CFLAGS = $(CCDLFLAGS) $(cflags) -fPIC $(ARCH_FLAG)
|
2.7.4 seems to have a bug – $(cflags)
is ignored, so we can’t specify -ggdb3 -O0
and the extension are always compiled with -g -O2
.
RbConfig::MAKEFILE_CONFIG["CFLAGS"] = fixed_CFLAGS
fixes this bug.
View on GitHub
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
$ ruby extconf.rb
Fix CFLAGS: -g -O2 -> $(cflags)
creating Makefile
$ make clean && make V=1
gcc -I. -I/home/wsh/.rbenv/versions/2.7.4-dbg/include/ruby-2.7.0/x86_64-linux -I/home/wsh/.rbenv/versions/2.7.4-dbg/include/ruby-2.7.0/ruby/backward -I/home/wsh/.rbenv/versions/2.7.4-dbg/include/ruby-2.7.0 -I. -fPIC -ggdb3 -o example_ext.o -c example_ext.c
rm -f example_ext.so
gcc -shared -o example_ext.so example_ext.o -L. -L/home/wsh/.rbenv/versions/2.7.4-dbg/lib -Wl,-rpath,/home/wsh/.rbenv/versions/2.7.4-dbg/lib -L. -fstack-protector-strong -rdynamic -Wl,-export-dynamic -Wl,--compress-debug-sections=zlib -lm -lc
$ make clean
$ cd ../../
$ bundle exec rake install
cd tmp/x86_64-linux/example_ext/2.7.4
/home/wsh/.rbenv/versions/2.7.4-dbg/bin/ruby -I. ../../../../ext/example_ext/extconf.rb
Fix CFLAGS: -g -O2 -> $(cflags)
creating Makefile
cd -
cd tmp/x86_64-linux/example_ext/2.7.4
/usr/bin/make
linking shared-object example_ext/example_ext.so
cd -
install -c tmp/x86_64-linux/example_ext/2.7.4/example_ext.so lib/example_ext/example_ext.so
cp tmp/x86_64-linux/example_ext/2.7.4/example_ext.so tmp/x86_64-linux/stage/lib/example_ext/example_ext.so
example_ext 0.1.0 built to pkg/example_ext-0.1.0.gem.
example_ext (0.1.0) installed.
|
Debug it with gdb
.
1
2
3
4
5
6
|
# (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/2.7.4-dbg/bin/ruby -e 'require "example_ext"; ExampleExt.hello'
# (c) not recommended:
gdb -q -ex 'set breakpoint pending on' -ex 'b example_hello' -ex run --args ~/.rbenv/versions/2.7.4-dbg/bin/ruby -e 'require "example_ext"; ExampleExt.hello'
|
Detailed explanation about the gdb invocation
In (a), with bundle exec
context, require "example_ext"
(indirectly) loads
/path/to/example_ext/example_ext.so
whose debug info refers to
/path/to/example_ext/ext/example_ext/example_ext.c
.
Without bundle exec
(b), gdb can’t load ruby
which is a shell script, so specify absolute path to Ruby binary.
And RUBY_LIB=./lib
is needed; otherwise (c), ~/.rbenv/versions/2.7.4-dbg/.../example_ext.so
will be loaded which refers to .rbenv/versions/2.7.4-dbg/.../example_ext.c
[1].
We can set substitute-path
gdb command to let gdb looks up the original sources.
Try commands below for further understanding.
1
2
3
4
5
6
7
8
9
|
echo $PATH
which -a ruby
bundle exec echo $PATH
bundle exec which -a ruby
file ~/.rbenv/shims/ruby
file ~/.rbenv/versions/2.7.4-dbg/bin/ruby
ruby -e 'p $:' # or $LOAD_PATH
bundle exec ruby -e 'p $:'
env RUBYLIB=./lib ruby -e 'p $:'
|
[1] The rake install
task executes the build
task and gem install /path/to/example_ext/pkg/example_ext-0.1.0.gem
command. gem install example_ext-0.1.0.gem
extracts the C sources to ~/.rbenv/versions/2.7.4-dbg/lib/ruby/gems/2.7.0/gems/example_ext-0.1.0/
and compiles them, so debug info for the source path points to ~/.rbenv/versions/2.7.4-dbg/lib/ruby/gems/2.7.0/gems/example_ext-0.1.0/ext/example_ext/example_ext.c
.
References:
Now we can debug the C extension!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
$ bundle exec gdb -q -ex 'set breakpoint pending on' -ex 'b example_hello' -ex run --args ruby -e 'require "example_ext"; ExampleExt.hello'
Reading symbols from ruby...
Function "example_hello" not defined.
Breakpoint 1 (example_hello) pending.
Starting program: /home/wsh/.rbenv/versions/2.7.4-dbg/bin/ruby -e require\ \"example_ext\"\;\ ExampleExt.hello
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[Detaching after vfork from child process 802638]
Breakpoint 1, example_hello (obj=93824995722496) at ../../../../ext/example_ext/example_ext.c:7
7 {
(gdb) list
2
3 VALUE rb_mExampleExt;
4
5 static VALUE
6 example_hello(VALUE obj)
7 {
8 printf("hello\n");
9
10 return Qnil;
11 }
(gdb) next
8 printf("hello\n");
(gdb) next
hello
10 return Qnil;
(gdb) next
11 }
(gdb) next
vm_call_cfunc_with_frame (empty_kw_splat=<optimized out>, cd=0x55555607f450, calling=<optimized out>, reg_cfp=0x7ffff71a9fa0, ec=0x5555558a8500) at ../vm_insnhelper.c:2516
2516 CHECK_CFP_CONSISTENCY("vm_call_cfunc");
(gdb) continue
Continuing.
[Inferior 1 (process 820270) exited normally]
(gdb) quit
|
References