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.
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 /
).
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."
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);
}
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.
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")
$ 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 $:'
<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