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

My Little C Extension: Lego Robots are Magic

tehviking
November 19, 2014

My Little C Extension: Lego Robots are Magic

Learn about C Extensions in Ruby, using the Raspberry Pi to control Lego Mindstorms, and dealing with burnout with your host, Brandon Hays.

tehviking

November 19, 2014
Tweet

More Decks by tehviking

Other Decks in Programming

Transcript

  1. We know you have a choice in conference tracks. Thank

    you for choosing ours. Welcome to the Fringe Ruby track.
  2. <3

  3. H it burnout I nspiration purchase P laytime S teal

    knowledge C heat to win A tone for your cheating F ind a practical application E njoy a Coca-Cola™
  4. 1. Create a gem http://guides.rubygems.org/make-your-own-gem/ $ bundle gem brick_pi -

    Edit .gemspec - Edit lib/brick_pi/version.rb - Add `gem 'rake-compiler'` to Gemfile
  5. 2. Lay filesystem groundwork http://guides.rubygems.org/gems-with-extensions/ - Create ext/brick_pi/ - Include

    C files needed for the robot (tick.h, BrickPi.h) - Create native.c - Create extconf.rb
  6. 2. Lay filesystem groundwork require "bundler/gem_tasks" require "rake/extensiontask" Rake::ExtensionTask.new("native", eval(File.read("brick_pi.gemspec")))

    do |ext| ext.ext_dir = "ext/brick_pi" ext.lib_dir = "lib/brick_pi" ext.source_pattern = "*.{c,h}" end Rakefile http://guides.rubygems.org/gems-with-extensions/
  7. 2. Lay filesystem groundwork Gem::Specification.new do |spec| spec.name = "brick_pi"

    spec.version = BrickPi::VERSION ... spec.license = "MIT" spec.require_paths = ["lib", "ext"] spec.extensions = ["ext/brick_pi/extconf.rb"] ... end .gemspec http://guides.rubygems.org/gems-with-extensions/
  8. “We go to war with the API we wish we

    had, not the API we have. - Robert Schuller
  9. 3. Specify Ruby API # I want this API: BrickPi::Native.brickPiSetup()

    # In Ruby it’d look like this: module BrickPi module Native def self.brickPiSetup() # do some C stuff here end end end
  10. # I want this API: BrickPi::Native.brickPiSetup() # In Ruby it’d

    look like this: module BrickPi module Native def self.brickPiSetup() # do some C stuff here end end end 3. Specify Ruby API Top namespace (from gem)
  11. # I want this API: BrickPi::Native.brickPiSetup() # In Ruby it’d

    look like this: module BrickPi module Native def self.brickPiSetup() # do some C stuff here end end end 3. Specify Ruby API “Native” namespace
  12. # I want this API: BrickPi::Native.brickPiSetup() # In Ruby it’d

    look like this: module BrickPi module Native def self.brickPiSetup() # do some C stuff here end end end 3. Specify Ruby API Setup method
  13. 4. Write first wrapper #include "ruby.h"; #include "tick.h"; #include "BrickPi.h";

    VALUE call_brickPiSetup(VALUE self) { return INT2FIX(BrickPiSetup()); } void Init_native() { VALUE BrickPi = rb_define_module("BrickPi"); VALUE Native = rb_define_module_under(BrickPi, "Native"); rb_define_singleton_method(Native, "brickPiSetup", call_brickPiSetup, 0); } ext/brick_pi/native.c
  14. #include "ruby.h"; #include "tick.h"; #include "BrickPi.h"; 4. Write first wrapper

    #include "ruby.h"; #include "tick.h"; #include "BrickPi.h"; VALUE call_brickPiSetup(VALUE self) { return INT2FIX(BrickPiSetup()); } void Init_native() { VALUE BrickPi = rb_define_module("BrickPi"); VALUE Native = rb_define_module_under(BrickPi, "Native"); rb_define_singleton_method(Native, "brickPiSetup", call_brickPiSetup, 0); } ext/brick_pi/native.c Ruby C libs Include:
  15. #include "ruby.h"; #include "tick.h"; #include "BrickPi.h"; 4. Write first wrapper

    #include "ruby.h"; #include "tick.h"; #include "BrickPi.h"; VALUE call_brickPiSetup(VALUE self) { return INT2FIX(BrickPiSetup()); } void Init_native() { VALUE BrickPi = rb_define_module("BrickPi"); VALUE Native = rb_define_module_under(BrickPi, "Native"); rb_define_singleton_method(Native, "brickPiSetup", call_brickPiSetup, 0); } ext/brick_pi/native.c C dependencies Include:
  16. #include "ruby.h"; #include "tick.h"; #include "BrickPi.h"; VALUE call_brickPiSetup(VALUE self) {

    return INT2FIX(BrickPiSetup()); } void Init_native() { VALUE BrickPi = rb_define_module("BrickPi"); VALUE Native = rb_define_module_under(BrickPi, "Native"); rb_define_singleton_method(Native, "brickPiSetup", call_brickPiSetup, 0); } void Init_native() { 4. Write first wrapper ext/brick_pi/native.c “magic” init_ method
  17. #include "ruby.h"; #include "tick.h"; #include "BrickPi.h"; VALUE call_brickPiSetup(VALUE self) {

    return INT2FIX(BrickPiSetup()); } void Init_native() { VALUE BrickPi = rb_define_module("BrickPi"); VALUE Native = rb_define_module_under(BrickPi, "Native"); rb_define_singleton_method(Native, "brickPiSetup", call_brickPiSetup, 0); } VALUE BrickPi = rb_define_module("BrickPi"); VALUE Native = rb_define_module_under(BrickPi, "Native"); 4. Write first wrapper ext/brick_pi/native.c module BrickPi module BrickPi:Native
  18. #include "ruby.h"; #include "tick.h"; #include "BrickPi.h"; VALUE call_brickPiSetup(VALUE self) {

    return INT2FIX(BrickPiSetup()); } void Init_native() { VALUE BrickPi = rb_define_module("BrickPi"); VALUE Native = rb_define_module_under(BrickPi, "Native"); rb_define_singleton_method(Native, "brickPiSetup", call_brickPiSetup, 0); } rb_define_singleton_method 4. Write first wrapper ext/brick_pi/native.c def self.brickPiSetup()
  19. #include "ruby.h"; #include "tick.h"; #include "BrickPi.h"; VALUE call_brickPiSetup(VALUE self) {

    return INT2FIX(BrickPiSetup()); } void Init_native() { VALUE BrickPi = rb_define_module("BrickPi"); VALUE Native = rb_define_module_under(BrickPi, "Native"); rb_define_singleton_method(Native, "brickPiSetup", call_brickPiSetup, 0); } Native, 4. Write first wrapper ext/brick_pi/native.c defined on
  20. #include "ruby.h"; #include "tick.h"; #include "BrickPi.h"; VALUE call_brickPiSetup(VALUE self) {

    return INT2FIX(BrickPiSetup()); } void Init_native() { VALUE BrickPi = rb_define_module("BrickPi"); VALUE Native = rb_define_module_under(BrickPi, "Native"); rb_define_singleton_method(Native, "brickPiSetup", call_brickPiSetup, 0); } "brickPiSetup" 4. Write first wrapper ext/brick_pi/native.c Ruby method name
  21. #include "ruby.h"; #include "tick.h"; #include "BrickPi.h"; VALUE call_brickPiSetup(VALUE self) {

    return INT2FIX(BrickPiSetup()); } void Init_native() { VALUE BrickPi = rb_define_module("BrickPi"); VALUE Native = rb_define_module_under(BrickPi, "Native"); rb_define_singleton_method(Native, "brickPiSetup", call_brickPiSetup, 0); } VALUE call_brickPiSetup call_brickPiSetup 4. Write first wrapper ext/brick_pi/native.c C helper method
  22. #include "ruby.h"; #include "tick.h"; #include "BrickPi.h"; VALUE call_brickPiSetup(VALUE self) {

    return INT2FIX(BrickPiSetup()); } void Init_native() { VALUE BrickPi = rb_define_module("BrickPi"); VALUE Native = rb_define_module_under(BrickPi, "Native"); rb_define_singleton_method(Native, "brickPiSetup", call_brickPiSetup, 0); } 0 4. Write first wrapper ext/brick_pi/native.c no Ruby args
  23. #include "ruby.h"; #include "tick.h"; #include "BrickPi.h"; VALUE call_brickPiSetup(VALUE self) {

    return INT2FIX(BrickPiSetup()); } void Init_native() { VALUE BrickPi = rb_define_module("BrickPi"); VALUE Native = rb_define_module_under(BrickPi, "Native"); rb_define_singleton_method(Native, "brickPiSetup", call_brickPiSetup, 0); } 4. Write first wrapper VALUE call_brickPiSetup ext/brick_pi/native.c wrapper/helper function
  24. #include "ruby.h"; #include "tick.h"; #include "BrickPi.h"; VALUE call_brickPiSetup(VALUE self) {

    return INT2FIX(BrickPiSetup()); } void Init_native() { VALUE BrickPi = rb_define_module("BrickPi"); VALUE Native = rb_define_module_under(BrickPi, "Native"); rb_define_singleton_method(Native, "brickPiSetup", call_brickPiSetup, 0); } 4. Write first wrapper BrickPiSetup() ext/brick_pi/native.c original C function
  25. #include "ruby.h"; #include "tick.h"; #include "BrickPi.h"; VALUE call_brickPiSetup(VALUE self) {

    return INT2FIX(BrickPiSetup()); } void Init_native() { VALUE BrickPi = rb_define_module("BrickPi"); VALUE Native = rb_define_module_under(BrickPi, "Native"); rb_define_singleton_method(Native, "brickPiSetup", call_brickPiSetup, 0); } 4. Write first wrapper INT2FIX ext/brick_pi/native.c cast C int to Ruby Fixnum
  26. #include "ruby.h"; #include "tick.h"; #include "BrickPi.h"; VALUE call_brickPiSetup(VALUE self) {

    return INT2FIX(BrickPiSetup()); } void Init_native() { VALUE BrickPi = rb_define_module("BrickPi"); VALUE Native = rb_define_module_under(BrickPi, "Native"); rb_define_singleton_method(Native, "brickPiSetup", call_brickPiSetup, 0); } 4. Write first wrapper (VALUE self) { 0 ext/brick_pi/native.c wait, what?
  27. #include "ruby.h"; #include "tick.h"; #include "BrickPi.h"; VALUE call_brickPiSetup(VALUE self) {

    return INT2FIX(BrickPiSetup()); } void Init_native() { VALUE BrickPi = rb_define_module("BrickPi"); VALUE Native = rb_define_module_under(BrickPi, "Native"); rb_define_singleton_method(Native, "brickPiSetup", call_brickPiSetup, 0); } Object-oriented C VALUE VALUE ext/brick_pi/native.c magic ‘VALUE’ type
  28. #include "ruby.h"; #include "tick.h"; #include "BrickPi.h"; VALUE call_brickPiSetup(VALUE self) {

    return INT2FIX(BrickPiSetup()); } void Init_native() { VALUE BrickPi = rb_define_module("BrickPi"); VALUE Native = rb_define_module_under(BrickPi, "Native"); rb_define_singleton_method(Native, "brickPiSetup", call_brickPiSetup, 0); } Object-oriented C self ext/brick_pi/native.c always pass self as 1st argument
  29. 4. Build gem & experiment $ rake compile $ cd

    ~/projects/brick_pi_test $ cat "gem 'brick_pi', '/users/pi/projects/brick_pi'" > Gemfile $ bundle exec irb > require 'brick_pi' > BrickPi::Native.brickPiSetup()
  30. Holy crap, it worked > BrickPi::Native.brickPiSetup() => 0 => Also

    hey nice and cool job you are a pretty good => programmer you should maybe do this for a living
  31. But wait, our Ruby file is empty! require "brick_pi/version" require

    "brick_pi/native" module BrickPi # Your code goes here... end lib/brick_pi.rb
  32. From here! void Init_native() { VALUE BrickPi = rb_define_module("BrickPi"); VALUE

    Native = rb_define_module_under(BrickPi, "Native"); rb_define_singleton_method(Native, "brick_pi_setup", call_brick_pi_setup, 0); } ext/brick_pi/native.c
  33. • Keep low-level Ruby ugly, close to C code as

    possible • Low-level wrappers in BrickPi::Native module • Prefix C method helpers for easy identification
  34. Keep low-level Ruby ugly, close to C code as possible

    rb_define_singleton_method(Native, "BrickPiUpdateValues", bprb_BrickPiUpdateValues, 0); BrickPi::Native.BrickPiUpdateValues() lib/brick_pi/bot.rb ext/brick_pi/native.c
  35. Low-level wrappers in BrickPi::Native module void Init_native() { VALUE BrickPi

    = rb_define_module("BrickPi"); VALUE Native = rb_define_module_under(BrickPi, "Native"); VALUE MotorSpeed = rb_define_module_under(Native, "MotorSpeed"); module BrickPi class Motor def spin(speed) speed = [-100, [100, speed].min].max motor_speed = (speed * 2.55).round Native::MotorSpeed[@port] = motor_speed end end end lib/brick_pi/motor.rb ext/brick_pi/native.c
  36. Prefix C method helpers for easy identification VALUE bprb_BrickPiSetup(VALUE self)

    { return INT2FIX(BrickPiSetup()); } VALUE bprb_BrickPiSetupSensors(VALUE self) { return INT2FIX(BrickPiSetupSensors()); } VALUE bprb_ClearTick(VALUE self) { return INT2FIX(ClearTick()); } VALUE bprb_MotorSpeed_set(VALUE self, VALUE key, VALUE value) { int index = FIX2INT(key); BrickPi.MotorSpeed[index] = FIX2INT(value); return value; } bprb, for brick_pi_ruby
  37. Putting it together VALUE bprb_SensorType_set(VALUE self, VALUE key, VALUE value)

    { int index = FIX2INT(key); BrickPi.SensorType[index] = FIX2INT(value); return value; } ... VALUE SensorType = rb_define_module_under(Native, "SensorType"); rb_define_singleton_method(SensorType, "[]=", bprb_SensorType_set, 2);
  38. Putting it together VALUE bprb_SensorType_set(VALUE self, VALUE key, VALUE value)

    { int index = FIX2INT(key); BrickPi.SensorType[index] = FIX2INT(value); return value; } ... VALUE SensorType = rb_define_module_under(Native, "SensorType"); rb_define_singleton_method(SensorType, "[]=", bprb_SensorType_set, 2); VALUE SensorType = rb_define_module_under(Native, "SensorType"); module Native::SensorType
  39. Putting it together VALUE bprb_SensorType_set(VALUE self, VALUE key, VALUE value)

    { int index = FIX2INT(key); BrickPi.SensorType[index] = FIX2INT(value); return value; } ... VALUE SensorType = rb_define_module_under(Native, "SensorType"); rb_define_singleton_method(SensorType, "[]=", bprb_SensorType_set, 2); rb_define_singleton_method(SensorType, "[]=", def self.[]=(key, val)
  40. Putting it together VALUE bprb_SensorType_set(VALUE self, VALUE key, VALUE value)

    { int index = FIX2INT(key); BrickPi.SensorType[index] = FIX2INT(value); return value; } ... VALUE SensorType = rb_define_module_under(Native, "SensorType"); rb_define_singleton_method(SensorType, "[]=", bprb_SensorType_set, 2); bprb_SensorType_set bprb_SensorType_set call to C helper/wrapper
  41. Putting it together VALUE bprb_SensorType_set(VALUE self, VALUE key, VALUE value)

    { int index = FIX2INT(key); BrickPi.SensorType[index] = FIX2INT(value); return value; } ... VALUE SensorType = rb_define_module_under(Native, "SensorType"); rb_define_singleton_method(SensorType, "[]=", bprb_SensorType_set, 2); VALUE self pass self 1st
  42. Putting it together VALUE bprb_SensorType_set(VALUE self, VALUE key, VALUE value)

    { int index = FIX2INT(key); BrickPi.SensorType[index] = FIX2INT(value); return value; } ... VALUE SensorType = rb_define_module_under(Native, "SensorType"); rb_define_singleton_method(SensorType, "[]=", bprb_SensorType_set, 2); VALUE key, VALUE value ... 2 2 Ruby arguments def self.[]=(key, val)
  43. Putting it together VALUE bprb_SensorType_set(VALUE self, VALUE key, VALUE value)

    { int index = FIX2INT(key); BrickPi.SensorType[index] = FIX2INT(value); return value; } ... VALUE SensorType = rb_define_module_under(Native, "SensorType"); rb_define_singleton_method(SensorType, "[]=", bprb_SensorType_set, 2); { int index = FIX2INT(key); FIX2INT(value) cast Fixnum in Ruby to INT in C
  44. Putting it together VALUE bprb_SensorType_set(VALUE self, VALUE key, VALUE value)

    { int index = FIX2INT(key); BrickPi.SensorType[index] = FIX2INT(value); return value; } ... VALUE SensorType = rb_define_module_under(Native, "SensorType"); rb_define_singleton_method(SensorType, "[]=", bprb_SensorType_set, 2); BrickPi.SensorType[index] = value Set value at position in C array
  45. Putting it together VALUE bprb_SensorType_set(VALUE self, VALUE key, VALUE value)

    { int index = FIX2INT(key); BrickPi.SensorType[index] = FIX2INT(value); return value; } ... VALUE SensorType = rb_define_module_under(Native, "SensorType"); rb_define_singleton_method(SensorType, "[]=", bprb_SensorType_set, 2); return value; return VALUE ‘value’ to Ruby
  46. void Init_native() { VALUE BrickPi = rb_define_module("BrickPi"); VALUE Native =

    rb_define_module_under(BrickPi, "Native"); rb_define_singleton_method(Native, "BrickPiSetup", bprb_BrickPiSetup, 0); rb_define_singleton_method(Native, "BrickPiSetupSensors", bprb_BrickPiSetupSensors, 0); rb_define_singleton_method(Native, "ClearTick", bprb_ClearTick, 0); VALUE MotorSpeed = rb_define_module_under(Native, "MotorSpeed"); rb_define_singleton_method(MotorSpeed, "[]=", bprb_MotorSpeed_set, 2); VALUE MotorEnable = rb_define_module_under(Native, "MotorEnable"); rb_define_singleton_method(MotorEnable, "[]=", bprb_MotorEnable_set, 2); VALUE Address = rb_define_module_under(Native, "Address"); rb_define_singleton_method(Address, "[]=", bprb_Address_set, 2); VALUE SensorType = rb_define_module_under(Native, "SensorType"); rb_define_singleton_method(SensorType, "[]=", bprb_SensorType_set, 2); VALUE Sensor = rb_define_module_under(Native, "Sensor"); rb_define_singleton_method(Sensor, "[]", bprb_Sensor_get, 1); VALUE Encoder = rb_define_module_under(Native, "Encoder"); rb_define_singleton_method(Encoder, "[]", bprb_Encoder_get, 1); Does this look repetitive?
  47. rb_define_const(Native, "PORT_A", INT2FIX(0)); rb_define_const(Native, "PORT_B", INT2FIX(1)); rb_define_const(Native, "PORT_C", INT2FIX(2)); rb_define_const(Native,

    "PORT_D", INT2FIX(3)); rb_define_const(Native, "PORT_1", INT2FIX(0)); rb_define_const(Native, "PORT_2", INT2FIX(1)); rb_define_const(Native, "PORT_3", INT2FIX(2)); rb_define_const(Native, "PORT_4", INT2FIX(3)); rb_define_const(Native, "US_PORT", INT2FIX(2)); rb_define_const(Native, "TYPE_SENSOR_TOUCH", INT2FIX(32)); rb_define_const(Native, "TYPE_SENSOR_ULTRASONIC_CONT", INT2FIX(33)); rb_define_const(Native, "TYPE_SENSOR_ULTRASONIC_SS", INT2FIX(34)); rb_define_const(Native, "TYPE_SENSOR_COLOR_FULL", INT2FIX(36)); rb_define_const(Native, "TYPE_SENSOR_COLOR_RED", INT2FIX(37)); rb_define_const(Native, "TYPE_SENSOR_COLOR_GREEN", INT2FIX(38)); rb_define_const(Native, "TYPE_SENSOR_COLOR_BLUE", INT2FIX(39)); rb_define_const(Native, "TYPE_SENSOR_COLOR_NONE", INT2FIX(40)); } That’s because it is repetitive.
  48. Why should the race always be to the swift, or

    the Jumble to the quick-witted? Should they be allowed to win merely because of the gifts God gave them? Well I say, "Cheating is the gift man gives himself." - C. Montgomery Burns
  49. Can you tell me all the secrets of C extension

    methods? Ok, then we can go get tacos.
  50. no. nonono. require 'brick_pi' include BrickPi motor1 = Native::PORT_A motor2

    = Native::PORT_B Native.ClearTick() Native.BrickPiSetup() Native::MotorSpeed[motor1] = 200 Native::MotorSpeed[motor2] = -200 Native::Address[0] = 1 Native::Address[1] = 2 Native::MotorEnable[motor1] = 1 Native::MotorEnable[motor2] = 1 Native.Timeout = 3000 Native.BrickPiSetTimeout() Native.BrickPiUpdateValues() pseudo.rb
  51. Better? require 'brick_pi' include BrickPi class BrickBot def start @stop

    = false ...setup code... Thread.new do until @stop do Native.BrickPiUpdateValues() sleep(50/1000) end end end def stop @stop = true end end pseudo.rb
  52. module BrickPi class Bot include BrickPi::Configuration attr_accessor :motor_A, :motor_B, :motor_C,

    :motor_D attr_accessor :sensor_1, :sensor_2, :sensor_3, :sensor_4 def initialize ...same old init code as before... end def start ...same old init code as before... end def stop @stop = true end def run start yield stop end end end
  53. # Instantiate the Bot bot = BrickPi.create do |bot| bot.motor

    :port_B bot.motor :port_C end # Get this party started bot.run do # Half speed on both motors. Max value is 100. bot.motor_B.spin 50 bot.motor_C.spin 50 sleep 5 # Stop motors bot.motor_B.stop bot.motor_C.stop end
  54. def tick self.class.devices.each { |d| d.zero } next_behaviors = []

    @behaviors.sort.each do |behavior| behavior.apply(next_behaviors) if behavior.active? next_behaviors << behavior end end @behaviors = next_behaviors self.class.properties.each { |p| p.apply self } end def behavior(priority=1, &block) Behavior.new(priority, &block).tap do |behavior| @behaviors << behavior end end def add_behavior(*behaviors) @behaviors.concat behaviors end def stop @stop = true @thread = nil end
  55. class BehaviorBot < BrickPi::Bot attr_accessor :speed motor :port_A motor :port_B

    ultrasonic_sensor :port_3 # A property is always settable and gettable, even while things are running. property :speed_left do |value| @motor_A.spin value end property :speed_right do |value| @motor_B.spin value end def move_forward(seconds) behavior do |time, done| self.speed_left = self.speed_right = speed done.call if time > seconds end end ... end
  56. @bot = BehaviorBot.new @bot.speed = 100 @bot.start # move forward

    for 4 seconds # then turn left for 3 seconds # then move forward for another 10 seconds @bot.move_forward(4).then do @bot.turn_left(3).then do @bot.move_backward(3).then do @bot.stop_motors(nil, true) end end end # Add a behavior to watch sensor and stop if motors are moving @bot.behavior(9) do |time, done| print "SENSOR DISTANCE: " puts @bot.sensor_3.distance if @bot.sensor_3.distance < 10 @bot.speed_left = @bot.speed_right = 0 end end @bot.wait
  57. What else can it do? # Crazy spirograph 12.times do

    @bot.forward 2 @bot.turn -140 end
  58. How did I manage to pour dozens of hours into

    learning C extensions while severely burned out?
  59. H it burnout I nspiration purchase P laytime S teal

    knowledge C heat to win A tone for your cheating F ind a practical application E njoy a Coca-Cola™
  60. Sunset Airplane Landing by dgmiami https://www.flickr.com/photos/dgmiami/ Yak by Chandan Kumar

    https://www.flickr.com/photos/ck-/ Hello World by Windell Oskay https://www.flickr.com/photos/oskay/ Stuck in the Mud by Jason Rogers https://www.flickr.com/photos/restlessglobetrotter/ Lego porn by Eric https://www.flickr.com/photos/ejpphoto/ Black box by thierry ehrmann https://www.flickr.com/photos/home_of_chaos/ Taco cat by Shannon Des Roches Rosa https://www.flickr.com/photos/shannonrosa/ Number Five is Alive by emilydickinsonridesabmx https://www.flickr.com/photos/emilyrides/ Rainbow Dash at BrickCon by Katie Walker https://www.flickr.com/photos/eilonwy77/ Elvis Diner by Rahel Jaskow https://www.flickr.com/photos/rahel_jaskow/ Mini Fluttershy by Pascal https://www.flickr.com/photos/pasukaru76/ Black Cover for Why's (Poignant) Guide to Ruby by Why the Lucky Stiff https://www.flickr.com/photos/_why/ Image credits