Share
Embed
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
Transcript
174 slides
-
We know you have a choice in conference tracks. Thank
you for choosing ours. Welcome to the Fringe Ruby track. -
ONE WEIRD TRICK the high clergy of Computerology don’t want
you to know... -
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™ -
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 -
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 -
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/ -
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/ -
“We go to war with the API we wish we
had, not the API we have. - Robert Schuller -
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 -
# 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) -
# 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 -
# 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 -
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 -
#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: -
#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: -
#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 -
#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 -
#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() -
#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 -
#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 -
#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 -
#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 -
#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 -
#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 -
#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 -
#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? -
#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 -
#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 -
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() -
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 -
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 -
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 -
• 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 -
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 -
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 -
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 -
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); -
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 -
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) -
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 -
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 -
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) -
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 -
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 -
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 -
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? -
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. -
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 -
Can you tell me all the secrets of C extension
methods? Ok, then we can go get tacos. -
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 -
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 -
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 -
# 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 -
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 -
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 -
@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 -
What else can it do? # Square 4.times do @bot.forward
2 @bot.turn -90 end -
What else can it do? # Crazy spirograph 12.times do
@bot.forward 2 @bot.turn -140 end -
How did I manage to pour dozens of hours into
learning C extensions while severely burned out? -
More importantly, how did that activity actually help alleviate and
even fix my burnout? -
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™ -
Sharing “play projects” is behind a huge portion of good
done in the world. -
Play is the only power fun enough to inspire the
next generation. -
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