Sunday 4 October 2009

The Amazing Ruby: Precompiled Header Hack for Eclipse CDT

With a good IDE, such as Eclipse, and good libraries, such as boost, programming in C++ is almost bearable. Unfortunately, boost's magic templates take a long time to compile. Like most compilers, g++ provides precompiled headers to mitigate this problem, but support for these in Eclipse has so far been limited. This post describes a hack to get some level of precompiled header support in Eclipse with the g++ tool chain on Linux.

How it Works

The usual build process for a C++ project in Eclipse is:

  1. Eclipse writes Makefiles based on the source files in your project, and your project's configuration settings.
  2. Eclipse calls make, and make builds your project.

Here we'll replace step 2 with:

  1. Eclipse calls a ruby script.
  2. The ruby script edits Eclipse's generated Makefiles to include build rules for the precompiled header.
  3. The ruby script calls make, and make builds your project, using the precompiled header as needed.

Using this approach, you can add and remove files from your project or change configuration settings, and things will work mostly as expected.

Caveats

The current limitations of this method are:

  1. The script only supports one precompiled header file per project. It works like Visual Studio, in this regard: you have to supply a header file (stdafx.h) that includes all the headers you want to precompile.

  2. You can only add precompiled header support to one configuration for each project; for example, if you have precompiled headers in the Debug configuration, they won't be used in the Release configuration. This is because the script puts the precompiled header into the source folder, rather than in the build folder.

  3. This is a hack. Your mileage may vary. It may not work in future versions of eclipse.

So, with those caveats in mind, the next section describes how to get it set up.

Tutorial

  1. Make sure you have ruby installed and that you can run it from a terminal (command prompt). These instructions have been tested with Ruby 1.8.7 on Ubuntu 9.04 (Jaunty). You can install ruby via the Synaptic package manager.

  2. Start eclipse. These instructions have been tested with Eclipse 3.5 SR1 (Galileo). I've also used this script with Eclipse 3.4 and 3.5.

  3. You can easily add this script to an existing project using the steps that follow. For this demo, I'll create a new Hello World project, to make things more concrete.

  4. Run the project to make sure it works, before changing things.

  5. Download the cdtgch.rb script. The script is also included in this post, below. Put a copy in the root of your project folder (workspace/cdtgchdemo in this case).

  6. Add a header file that includes the headers you want precompiled. The script assumes that you call it stdafx.h, for consistency with Visual Studio. It also assumes that you have put it in a folder called src, for consistency with the default Eclipse C++ project structure. (See step 8 for a picture of the file structure used in this demo.) You can use any name and folder structure you want, but you will have to edit the first few lines of cdtgch.rb accordingly.

  7. Now it's time to tell eclipse to call cdtgch.rb instead of make. Right click on the project in the Project Explorer and choose Properties. On the left, choose C/C++ Build. Uncheck the Use default build command option, and enter

    ruby ../cdtgch.rb

    as the build command. The ../ is important, because the command runs in the build directory, and the script is in the project root. Click OK.

    Note: This hack only allows you to use precompiled headers with one configuration per project. Here, we'll put it into the Debug configuration, which makes sense, because that's the one that gets built most often (sigh).

  8. Build the project. (Choose Project > Build All.) If you refresh the Project Explorer, you should find a stdafx.h.gch file along side your stdafx.h file; this is the precompiled header. It will be rebuilt every time you change stdafx.h.

  9. Make sure you change your source files to include stdafx.h. It should be the first file that you include. For example, my main C++ file now reads:


    #include "stdafx.h" // was #include <iostream>
    // add other includes here...


    using namespace std;

    int main() {
    cout << "!!!Hello World!!!" << endl; // prints !!!Hello World!!!
    return 0;
    }

    and my stdafx.h now reads:


    #ifndef STDAFX_H_
    #define STDAFX_H_

    #include <iostream> // was in main file

    #endif /* STDAFX_H_ */
  10. Continue developing as normal (but somewhat faster!).

The Ruby Script (cdtgch.rb)


#
# Edits Eclipse's generated Makefiles to make them support a precompiled header.
#
# To use this script:
# 1. Create a file that includes the headers you want to precompile; this script
# assumes that you use my_project/src/stdafx.h, but you can change the PCH
# constant, below, if you want to do it differently.
# 2. Put this script in your project root.
# 3. Edit your project's properties; under C/C++ build, set the build command to
# ruby ../cdtgch.rb
# (the "../" is important because eclipse runs the script from the build
# directory, and the script is in the project root).
# 4. Make sure your source files include "stdafx.h"; it's a good idea to make
# this the first include.
# 5. Everything else should be as normal.
#
# Note that the script only allows you to have *one precompiled header file*.
# Note that you only get to use the gch in *one configuration*.
#
# See
# http://jdleesmiller.blogspot.com/2009/10/amazing-ruby-precompiled-header-hack.html
# for more info.
#

#
# Header file that you want to precompile.
# Path must be relative to the project root.
# The directory containing the stdafx.h must contain at least one .cpp file.
#
PCH = 'src/stdafx.h'

# Look at existing makefile to see if it needs hacking; we will only rewrite
# it if necessary.
rewrite_makefile = false
makefile_lines = IO.readlines('makefile')
makefile_lines.each {|l| l.chomp!}

# Need to import dependency file for the pch. This has to happen after we've
# imported subdir.mk.
dep_line = "CPP_DEPS += #{PCH}.gch.d"
objects_line = makefile_lines.index("-include objects.mk")
raise "cannot find subdir.mk include line" unless objects_line
unless makefile_lines[objects_line+1] == dep_line
makefile_lines.insert(objects_line+1, dep_line)
rewrite_makefile = true
end

# Make all objects depend on the precompiled header (even if not all of them
# really do).
gch_o_rule = "$(OBJS):%.o:../#{PCH}.gch"
unless makefile_lines.member?(gch_o_rule)
makefile_lines << ""
makefile_lines << gch_o_rule
makefile_lines << ""

rewrite_makefile = true
end

# Look for the rule to build the gch. We need to use the same g++ arguments as
# everywhere else; we can get these from a subdir.mk file. The dependencies on
# the project files ensure that the PCH gets rebuilt when the project settings
# change; otherwise, this happens for all the other files, but not the PCH, for
# reasons that I don't fully understand.
gch_rule = "../#{PCH}.gch: ../#{PCH} ../.cproject ../.project"
unless makefile_lines.find {|l| l =~ /^#{gch_rule}/}
# Need to look up the command in the subdir.mk file.
subdir_mk = File.new(File.join(File.dirname(PCH),'subdir.mk')).read
subdir_mk =~ /^(\tg\+\+.*)$/ or raise "cannot find g++ command in subdir.mk"
cmd = $1

# Make the command do dependencies for the gch file.
cmd.gsub! /-MF"[^"]*"/, "-MF\"#{PCH}.gch.d\""
cmd.gsub! /-MT"[^"]*"/, "-MT\"#{PCH}.gch.d\""

# Append a rule for building the precompiled header.
makefile_lines<<""
makefile_lines<<"#{gch_rule}"
makefile_lines<<cmd

rewrite_makefile = true
end

# Add a command to the clean rule so we get rid of the gch-related files.
clean_line = (0...makefile_lines.size).find{|i| makefile_lines[i] =~ /^clean:/}
raise "couldn't find clean: line in makefile" unless clean_line
unless makefile_lines[clean_line+1] =~ /#{PCH}\.gch/
makefile_lines.insert(clean_line+1,
"\trm -f #{PCH}.gch.d ../#{PCH}.gch")
rewrite_makefile = true
end

# Save changes, if any.
if rewrite_makefile
File.open('makefile', 'w') do |f|
f.write makefile_lines.join("\n")
end
end

# Now run make.
exec "make", ARGV.join(' ')

#
# Copyright (c) 2009 John Lees-Miller
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without
# restriction, including without limitation the rights to use,
# copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following
# conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
#

2 comments:

punkonlife said...

I think the newer versions of Eclipse use a slightly different directory setup. When I try using this script, I get the following error, despite the fact that stdafx.h is indeed in the src directory. Do you have an update?

g++: error: ../src/stdafx.h
: No such file or directory
g++: fatal error: no input files
compilation terminated.
makefile:67: recipe for target '../src/stdafx.h.gch' failed
make: *** [../src/stdafx.h.gch] Error 1

Unknown said...

Thanks for getting in touch. I'm afraid I haven't used this script in many years now, so I can't say off hand what modifications would be needed to get it to work in the most recent version of Eclipse. If you figure it out, please do post a comment (or do a blog post --- I can link to it as an update). It might also help to post the question on Stack Overflow.