Slide 1

Slide 1 text

CAMERON DUTRO • 7/18/2024 LET’S WRITE A C EXTENSION!

Slide 2

Slide 2 text

WHO IS THIS GUY? Cameron Dutro GitHub, Inc github.com/camertron @camertron@ruby.social

Slide 3

Slide 3 text

- 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?

Slide 4

Slide 4 text

- Performance - Wrap a native library like imagemagick, libsqlite3, or libxml - Low-level access to hardware - Just for fun! WHY WRITE A C EXTENSION?

Slide 5

Slide 5 text

- 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) # => #

Slide 6

Slide 6 text

THIS IS A TERRIBLE IDEA

Slide 7

Slide 7 text

- Ruby objects are always of one type - Impossible to treat a class as an instance of its supertype DEAR GOD WHY

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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 :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 `' 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 `'

Slide 10

Slide 10 text

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(); } }

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

DIRECTORY STRUCTURE ┌── Rakefile ├── ext │ └── transfigure │ ├── extconf.rb │ └── transfigure.c ├── lib │ ├── transfigure │ │ └── version.rb │ └── transfigure.rb ├── spec │ ├── spec_helper.rb │ └── transfigure_spec.rb │ └── transfigure.gemspec

Slide 13

Slide 13 text

GEMFILE source "https://rubygems.org" gemspec group :development do gem "rake" gem "rake-compiler" end

Slide 14

Slide 14 text

EXT/TRANSFIGURE/EXTCONF.RB require 'mkmf' $srcs = ["transfigure.c"] create_makefile("transfigure/transfigure")

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

- 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

Slide 17

Slide 17 text

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 ); }

Slide 18

Slide 18 text

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 # to be a kind of Bar # ./spec/transfigure_spec.rb:23:in `block (2 levels) in '

Slide 19

Slide 19 text

- 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

Slide 20

Slide 20 text

GETTING AN OBJECT’S CLASS VALUE value = (VALUE)fp->_bf._base; if (RBASIC(value)->klass) { // ... } Hmm, this looks promising!

Slide 21

Slide 21 text

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))

Slide 22

Slide 22 text

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; };

Slide 23

Slide 23 text

- 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?

Slide 24

Slide 24 text

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 ); }

Slide 25

Slide 25 text

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; ~~~~~~~~~~~~~~~~~~~ ^

Slide 26

Slide 26 text

No content

Slide 27

Slide 27 text

TRANSLATION Bro, that fi eld can’t be changed. It’s marked as constant. “ ” The C Compiler

Slide 28

Slide 28 text

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; };

Slide 29

Slide 29 text

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; };

Slide 30

Slide 30 text

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; };

Slide 31

Slide 31 text

No content

Slide 32

Slide 32 text

- 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

Slide 33

Slide 33 text

- 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);

Slide 34

Slide 34 text

COMPILER BE LIKE

Slide 35

Slide 35 text

STRUCTS IN C With great power comes great responsibility. “ ” Dennis Ritchie, probably

Slide 36

Slide 36 text

- 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; };

Slide 37

Slide 37 text

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 ); }

Slide 38

Slide 38 text

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 ); }

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

😎 THANKS FOR LISTENING!