Rack is a bridge between web servers and web applications. Most of the
web frameworks in Ruby are based on Rack. In this article, I'd like
to explore and demystify how its rackup tool works.
Diving deep into its source code revealed that some eval wizardry is
being used. Here's a sneak-peak of its beauty.
# Evaluate the given +builder_script+ string in the context of# a Rack::Builder block, returning a Rack application.defself.new_from_string(builder_script,file="(rackup)")# We want to build a variant of TOPLEVEL_BINDING with self as a Rack::Builder instance.# We cannot use instance_eval(String) as that would resolve constants differently.binding,builder=TOPLEVEL_BINDING.eval('Rack::Builder.new.instance_eval { [binding, self] }')evalbuilder_script,binding,filebuilder.to_append
I'll get to it later and explain how it works and why it's needed.
For now, let's get started with a "Hello World" example:
# in config.rurun(proc{[200,{'Content-Type'=>'text/plain'},['Hello World']]})
Running this with rackup will start Webrick and serve our little "Hello World" app on port 9292.
Getting started
It looks like running the rackup command somehow started the server and provided the run method to the top-level context. Let's find where it's coming from.
Looking at Rack's source code we discover this file sitting in a bin/ directory:
defstart(&block)ifoptions[:warn]$-w=trueendifincludes=options[:include]$LOAD_PATH.unshift(*includes)endArray(options[:require]).eachdo|library|requirelibraryendifoptions[:debug]$DEBUG=truerequire'pp'poptions[:server]ppwrapped_appppappendcheck_pid!ifoptions[:pid]# Touch the wrapped app, so that the config.ru is loaded before# daemonization (i.e. before chdir, etc).handle_profiling(options[:heapfile],options[:profile_mode],options[:profile_file])dowrapped_appenddaemonize_appifoptions[:daemonize]write_pidifoptions[:pid]trap(:INT)doifserver.respond_to?(:shutdown)server.shutdownelseexitendendserver.run(wrapped_app,**options,&block)end
Let's ignore server.run for a bit and dive deeper into the wrapped_app.
defbuild_app_and_options_from_configif!::File.exist?options[:config]abort"configuration #{options[:config]} not found"endapp,options=Rack::Builder.parse_file(self.options[:config],opt_parser)@options.merge!(options){|key,old,new|old}append
This leads us to Rack::Builder.parse_file which explains it's purpose in the comments.
# Parse the given config file to get a Rack application.## If the config file ends in +.ru+, it is treated as a# rackup file and the contents will be treated as if# specified inside a Rack::Builder block, using the given# options.## If the config file does not end in +.ru+, it is# required and Rack will use the basename of the file# to guess which constant will be the Rack application to run.# The options given will be ignored in this case.## Examples:## Rack::Builder.parse_file('config.ru')# # Rack application built using Rack::Builder.new## Rack::Builder.parse_file('app.rb')# # requires app.rb, which can be anywhere in Ruby's# # load path. After requiring, assumes App constant# # contains Rack application## Rack::Builder.parse_file('./my_app.rb')# # requires ./my_app.rb, which should be in the# # process's current directory. After requiring,# # assumes MyApp constant contains Rack applicationdefself.parse_file(config,opts=Server::Options.new)ifconfig.end_with?('.ru')returnself.load_file(config,opts)elserequireconfigapp=Object.const_get(::File.basename(config,'.rb').split('_').map(&:capitalize).join(''))returnapp,{}endend
Our file ends with .ru, so let's look at Rack::Builder.load_file.
# Load the given file as a rackup file, treating the# contents as if specified inside a Rack::Builder block.## Treats the first comment at the beginning of a line# that starts with a backslash as options similar to# options passed on a rackup command line.## Ignores content in the file after +__END__+, so that# use of +__END__+ will not result in a syntax error.## Example config.ru file:## $ cat config.ru## #\ -p 9393## use Rack::ContentLength# require './app.rb'# run Appdefself.load_file(path,opts=Server::Options.new)options={}cfgfile=::File.read(path)cfgfile.slice!(/\A#{UTF_8_BOM}/)ifcfgfile.encoding==Encoding::UTF_8ifcfgfile[/^#\\(.*)/]&&optswarn"Parsing options from the first comment line is deprecated!"options=opts.parse!$1.split(/\s+/)endcfgfile.sub!(/^__END__\n.*\Z/m,'')app=new_from_stringcfgfile,pathreturnapp,optionsend
Looks like Builder.new_from_string is the key for getting to bottom of this. Let's look it up.
# Evaluate the given +builder_script+ string in the context of# a Rack::Builder block, returning a Rack application.defself.new_from_string(builder_script,file="(rackup)")# We want to build a variant of TOPLEVEL_BINDING with self as a Rack::Builder instance.# We cannot use instance_eval(String) as that would resolve constants differently.binding,builder=TOPLEVEL_BINDING.eval('Rack::Builder.new.instance_eval { [binding, self] }')evalbuilder_script,binding,filebuilder.to_append
Wow, look at that! This is where the magic happens. Let's dive into it.
Double eval magic
This code makes Rack::Builder instance a top level context and evaluates
our script within that context. This provides the run method to our config.ru script, which is
actually defined in Rack::Builder#run[1].
TOPLEVEL_BINDING is, simply, a binding of the top-level context[2].
Why do we need it here? Couldn't we simply write:
I was confused by this, so I've asked the author, Benoit Daloze, to explain it and he did. Not having
TOPLEVEL_BINDING would give the Rack namespace to constants defined in our config.ru:
# config.rupBuilder# would print out Rack::Builder
That's why we need to evaluate within TOPLEVEL_BINDING.
A simpler, and a previously used version of this magic is:
However, this means that magic comments from config.ru would be ignored, since they are not present at the beginning of the eval-ed string. We would have to check for them and add them at the beginning of the string like this:
Calling run in our config.ru script, means that our journey continues to Rack::Builder#run:
# Takes an argument that is an object that responds to #call and returns a Rack response.# The simplest form of this is a lambda object:## run lambda { |env| [200, { "Content-Type" => "text/plain" }, ["OK"]] }## However this could also be a class:## class Heartbeat# def self.call(env)# [200, { "Content-Type" => "text/plain" }, ["OK"]]# end# end## run Heartbeatdefrun(app)@run=append
Since there are no other method calls to follow, it looks like we've come the end. Let's continue looking at branches that we've ignored previously.
The last thing we ignored was build_app in wrapped_app[↗]:
defserver@_server||=Rack::Handler.get(options[:server])unless@_server@_server=Rack::Handler.default# We already speak FastCGI@ignore_options=[:File,:Port]if@_server.to_s=='Rack::Handler::FastCGI'end@_serverend
The default Rack handler is Webrick, so in our case, server.run is actually Rack::Handler::Webrick.run:
This simply starts the Webrick server and passes our middleware to it as app.
And that's it! This article hopefully demystified what happens behind the curtain when you run rackup and you hopefully better understand how it works now.
Notes and examples
Here's the simplified version of what Rack::Builder.new_from_string is doing:
The first argument for eval is our
simplified config.ru. Running this will print out "testing".
I'm just kidding. It's not "simply". Let me explain what the hell I am talking about. Ruby documentation says that Binding "encapsulates the execution context at some particular place in the code and retain this context for future use". TOPLEVEL_BINDING is simply Binding of the top level-context. Here's an example:
foo=42defprint_fooprintfoo# undefined local variable or method `foo' for main:Objectendbegin;print_foo;rescue=>ex;putsex.message;enddefprint_foo_with_top_level_bindingprintTOPLEVEL_BINDING.eval("foo")# 42endprint_foo_with_top_level_binding
Want to talk more about this or any other topic? Email me. I welcome every email.