Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Cameron Dutro · Let's write a C Extension · SF ...

Cameron Dutro · Let's write a C Extension · SF Bay Area Ruby Meetup · July 18, 2024

Irina Nazarova

July 19, 2024
Tweet

More Decks by Irina Nazarova

Other Decks in Programming

Transcript

  1. - Ruby (well, MRI) is written in C. - You

    can extend Ruby by writing extensions in C. - An extension is a C program that you can use from Ruby. - RubyGems automatically compiles extensions on installation. WHAT ARE C EXTENSIONS?
  2. - Performance - Wrap a native library like imagemagick, libsqlite3,

    or libxml - Low-level access to hardware - Just for fun! WHY WRITE A C EXTENSION?
  3. - Create a gem with a native extension - The

    extension allows changing an object’s class at runtime - Code at github.com/camertron/trans fi gure OUR GOAL class Foo; end class Bar; end Foo.new.transfigure_into!(Bar) # => #<Bar...>
  4. - Ruby objects are always of one type - Impossible

    to treat a class as an instance of its supertype DEAR GOD WHY
  5. DEAR GOD WHY class Grandparent def foo = "grandparent foo"

    end class Parent < Grandparent def foo = method(:foo).super_method.call end class Child < Parent def foo = method(:foo).super_method.call end Child.new.foo
  6. DEAR GOD WHY (irb):6:in `call': stack level too deep (SystemStackError)

    from (irb):6:in `foo' from (irb):6:in `call' from (irb):6:in `foo' from (irb):6:in `call' from (irb):6:in `foo' from (irb):6:in `call' from (irb):6:in `foo' from (irb):6:in `call' ... 11884 levels... from <internal:kernel>:187:in `loop' from /Users/camertron/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/ irb-1.14.0/exe/irb:9:in `<top (required)>' from /Users/camertron/.asdf/installs/ruby/3.3.0/bin/irb:25:in `load' from /Users/camertron/.asdf/installs/ruby/3.3.0/bin/irb:25:in `<main>'
  7. DEAR GOD WHY class Grandparent { public String foo() {

    return "grandparent foo"; } } class Parent extends Grandparent { public String foo() { return super.foo(); } } class Child extends Parent { public String foo() { return super.foo(); } }
  8. DEAR GOD WHY class Grandparent def foo = "grandparent foo"

    end class Parent < Grandparent def foo = self.as(Grandparent).foo end class Child < Parent def foo = self.as(Parent).foo end Child.new.foo
  9. DIRECTORY STRUCTURE ┌── Rakefile ├── ext │ └── transfigure │

    ├── extconf.rb │ └── transfigure.c ├── lib │ ├── transfigure │ │ └── version.rb │ └── transfigure.rb ├── spec │ ├── spec_helper.rb │ └── transfigure_spec.rb │ └── transfigure.gemspec
  10. FIRST, SOME TESTS describe "transfigure_into!" do class Foo end class

    Bar end it "dynamically changes the object's class" do obj = Foo.new expect(obj).to be_a(Foo) obj.transfigure_into!(Bar) expect(obj).to be_a(Bar) end end
  11. - Ruby objects are stored in instances of the VALUE

    struct - nil is represented by a VALUE called Qnil - Ruby methods are de fi ned via the rb_define_method function. - Ruby methods de fi ned in C receive a VALUE called self as their fi rst argument - You can grab a reference to Ruby’s Object class via rb_cObject. - Native extensions de fi ne an initialization function that’s called when the extension is required THE RUBY C API
  12. EXT/TRANSFIGURE/TRANSFIGURE.C #include "ruby.h" VALUE tf_transfigure_into_bang(VALUE self, VALUE target_klass) { return

    Qnil; } void Init_transfigure() { rb_define_method( rb_cObject, "transfigure_into!", RUBY_METHOD_FUNC(tf_transfigure_into_bang), 1 ); }
  13. LET’S GIVE IT A TRY $> bundle exec rake compile

    $> bundle exec rake spec Failures: 1) transfigure_into! dynamically changes the object's class Failure/Error: expect(obj).to be_a(Bar) expected #<Foo:0x000000010011f188> to be a kind of Bar # ./spec/transfigure_spec.rb:23:in `block (2 levels) in <top (required)>'
  14. - Figure out how Ruby keeps track of the class

    of an object - Replace the class with the one we’re passed - Pro fi t! OUR MISSION
  15. GETTING AN OBJECT’S CLASS /** * Convenient casting macro. *

    * @param obj Arbitrary Ruby object. * @return The passed object casted to ::RBasic. */ #define RBASIC(obj) RBIMPL_CAST((struct RBasic *)(obj))
  16. GETTING AN OBJECT’S CLASS /** * Ruby object's base components.

    All Ruby objects have them in common. */ struct RUBY_ALIGNAS(SIZEOF_VALUE) RBasic { /** * Per-object flags... */ VALUE flags; /** * Class of an object. Every object has its class. Also, everything * is an object in Ruby... */ const VALUE klass; };
  17. - Use the RBASIC macro to cast our VALUE into

    an RBasic struct. - Set the klass fi eld on the RBasic struct. - Pro fi t! …IS THAT IT?
  18. LET’S GIVE IT A TRY #include "ruby.h" VALUE tf_transfigure_into_bang(VALUE self,

    VALUE target_klass) { RBASIC(self)->klass = target_klass; return Qnil; } void Init_transfigure() { rb_define_method( rb_cObject, "transfigure_into!", RUBY_METHOD_FUNC(tf_transfigure_into_bang), 1 ); }
  19. LET’S GIVE IT A TRY $> bundle exec rake compile

    ../../../../ext/transfigure/transfigure.c:17:25: error: cannot assign to non-static data member 'klass' with const-qualified type 'const VALUE' (aka 'const unsigned long') RBASIC(self)->klass = target_klass; ~~~~~~~~~~~~~~~~~~~ ^
  20. REMEMBER THIS? /** * Ruby object's base components. All Ruby

    objects have them in common. */ struct RUBY_ALIGNAS(SIZEOF_VALUE) RBasic { /** * Per-object flags... */ VALUE flags; /** * Class of an object. Every object has its class. Also, everything * is an object in Ruby... */ const VALUE klass; };
  21. REMEMBER THIS? /** * Ruby object's base components. All Ruby

    objects have them in common. */ struct RUBY_ALIGNAS(SIZEOF_VALUE) RBasic { /** * Per-object flags... */ VALUE flags; /** * Class of an object. Every object has its class. Also, everything * is an object in Ruby... */ const VALUE klass; };
  22. D’OH! /** * Ruby object's base components. All Ruby objects

    have them in common. */ struct RUBY_ALIGNAS(SIZEOF_VALUE) RBasic { /** * Per-object flags... */ VALUE flags; /** * Class of an object. Every object has its class. Also, everything * is an object in Ruby... */ const VALUE klass; };
  23. - Members of a struct are laid out sequentially in

    memory. - Accessing fi elds of a struct is just a bit of math. - Memory address of struct + byte o ff set = address of desired fi eld STRUCTS IN C FLAGS KLASS STRUCT RBASIC } unsigned long (32 bits) } unsigned long (32 bits) Address of klass is struct address + 32
  24. - Casting changes the type of a variable. - There

    are essentially no guardrails or rules for casting in C. - You can cast anything to anything else. CASTING IN C char *foo = "foo"; printf("%d", (int)foo);
  25. - We need to trick the compiler into thinking the

    klass member isn’t constant. - What if we de fi ne our own struct and cast to it? - Memory is laid out the same as the original struct. TRICKING THE COMPILER struct RUBY_ALIGNAS(SIZEOF_VALUE) TFRBasic { VALUE flags; VALUE klass; };
  26. LET’S GIVE IT A TRY #include "ruby.h" VALUE tf_transfigure_into_bang(VALUE self,

    VALUE target_klass) { ((struct TFRBasic*)RBASIC(self))->klass = target_klass; return Qnil; } void Init_transfigure() { rb_define_method( rb_cObject, "transfigure_into!", RUBY_METHOD_FUNC(tf_transfigure_into_bang), 1 ); }
  27. LET’S GIVE IT A TRY #include "ruby.h" VALUE tf_transfigure_into_bang(VALUE self,

    VALUE target_klass) { // Forgive me father, for I have sinned. ((struct TFRBasic*)RBASIC(self))->klass = target_klass; return Qnil; } void Init_transfigure() { rb_define_method( rb_cObject, "transfigure_into!", RUBY_METHOD_FUNC(tf_transfigure_into_bang), 1 ); }
  28. DOES IT WORK NOW? $> bundle exec rake spec ..

    Finished in 0.00143 seconds (files took 0.03876 seconds to load) 2 examples, 0 failures $> bundle exec rake compile