This article will show you how to integrate a C library in a Rubymotion OSX project. We are going to build an app that lets you render a markdown text into HTML using the C reference implementation of the CommonMark project.
You can find the full source code of the finished project over on Github
Last Updated: 18 May 2019 - corrected Brett Simmons to Brett Walker
There is also a sample project showing how to call Object-C classes from Rubymotion here (thanks Amir for pointing this out) and a related blog post from Clay Allsop.
Although I have got a few years of programming in Rubymotion, I always thought of vendoring C libraries as some kind of black magic, but it is easier than it seems.
Aside: If you would like to build an app with Markdown rendering in Rubymotion you might also want to check out Brett Walker’s @digitalmoksha
gem motion-markdown-it. Brett built his fantastic app VersatilMarkdown on this technologoy.
Here is a quick overview of what we are going to do:
- Create the Rubymotion project
- Download and build cmark
- Vendor the cmark library in the Rubymotion project
- Using cmark from the app
- Wrap things up
Creating the Rubymotion project
Let’s get started with our new project. Open up the Terminal, and fire
$ motion create --template=osx mdmotion
You should see something like:
[!] You may want to run `motion repo` if it's been a while since you've updated templates.
Create mdmotion
Create mdmotion/.gitignore
Create mdmotion/Gemfile
Create mdmotion/README.md
Create mdmotion/Rakefile
Create mdmotion/app/app_delegate.rb
Create mdmotion/app/menu.rb
Create mdmotion/resources/Assets.xcassets/AppIcon.appiconset/1024x1024.png
Create mdmotion/resources/Assets.xcassets/AppIcon.appiconset/Contents.json
Create mdmotion/resources/Assets.xcassets/AppIcon.appiconset/Icon_128x128.png
Create mdmotion/resources/Assets.xcassets/AppIcon.appiconset/Icon_128x128@2x.png
Create mdmotion/resources/Assets.xcassets/AppIcon.appiconset/Icon_16x16.png
Create mdmotion/resources/Assets.xcassets/AppIcon.appiconset/Icon_16x16@2x.png
Create mdmotion/resources/Assets.xcassets/AppIcon.appiconset/Icon_256x256.png
Create mdmotion/resources/Assets.xcassets/AppIcon.appiconset/Icon_256x256@2x.png
Create mdmotion/resources/Assets.xcassets/AppIcon.appiconset/Icon_32x32.png
Create mdmotion/resources/Assets.xcassets/AppIcon.appiconset/Icon_32x32@2x.png
Create mdmotion/resources/Assets.xcassets/AppIcon.appiconset/Icon_512x512.png
Create mdmotion/resources/Assets.xcassets/AppIcon.appiconset/Icon_512x512@2x.png
Create mdmotion/resources/Assets.xcassets/Contents.json
Create mdmotion/resources/Credits.rtf
Create mdmotion/spec/main_spec.rb
To see if everything is working, cd into the project directory and build the application.
$ cd mdmotion
$ bundle
$ bundle exec rake
This should give you a blank application window, which shows that you succesfully created the basic project.
Downloading and building cmark
Next we will download the cmark project. Go over to the project site on Github and click on the “Clone or download” button and download the project as a .zip file. Of course you could also clone the repo if you prefer that.
Double-click the .zip file in the Finder to unpack. This will result in a cmark-master
folder. Then go back to the Terminal and cd into the cmark-master
dir e.g. like so:
$ cd ~/Downloads/cmark-master
The cmark download doesn’t contain a finished build, so it is our task to create that. Therefore we follow along with the installation instructions in the cmark README and perform the following steps:
$ mkdir build # create the build dir
$ cd build # move to the build dir
$ cmake .. # create the Makefile
$ make # build cmark
The building process will take some time to complete. Please make sure that you have XCode and the XCode Command line tools installed for this step to work. If this step ran smoothly you have just successfully built cmark.
Vendoring the library in Rubymotion
Now we need to get the library into our Rubymotion project. First of all we need to copy over the built cmark library into the project. Let’s get back to the mdmotion project folder and run the following commands:
$ cd mypath/mdmotion # please exchange mypath with the path to your project dir
$ mkdir vendor # create a vendor dir for all third-party libraries
$ mkdir vendor/cmark # add a cmark dir to the vendor dir
$ cp -R ~/Downloads/cmark-master/build/src/* ./vendor/cmark/ # copy the files
$ cp ~/Downloads/cmark-master/src/cmark.h ./vendor/cmark/ # copy the header file
$ ls vendor/cmark # list the files
Of the steps above it is very important to copy over the cmark.h
header file, as we will see later.
The list command should give you something like:
$ ls vendor/cmark
cmark cmark.3 cmark_export.h libcmark.a
cmark-release.cmake cmark.cmake cmark_version.h libcmark.dylib
cmark.1 cmark.h libcmark.0.28.3.dylib libcmark.pc
Now that we put all the necessary stuff there we need to tell Rubymotion that it must include the libraries at runtime. Therefore we will edit the Rakefile and add the following line:
Motion::Project::App.setup do |app|
# Use `rake config' to see complete project settings.
app.name = 'mdmotion'
app.deployment_target = '10.13'
app.info_plist['CFBundleIconName'] = 'AppIcon'
# HERE COMES THE IMPORTANT CMARK STUFF
app.vendor_project("vendor/cmark", :static)
end
Before we try if the project builds, we will do a rake clean and then build. You could probably also leave away the cleaning, but according to my experience cleaning is a good way to avoid errors when updating vendored libraries or also CocoaPods.
$ bundle exec rake clean:all
…
$ bundle exec rake
If the app builds and you see the empty application window again it might look disappointing, but actually this is a good sign. Now we need to implement the actual application code to convert Markdown to HTML…
Using cmark from the app
The tricky part of using cmark - and any other C library - in Rubymotion is to find out how to call methods and create objects which are provided by the library. If we are looking for classes there is cool way to do that. We start the application and then print all the available classes from the repl command line.
$ bundle exec rake
… # lots of build output here
(main)> Kernel.constants
=> [:NSLeafProxy, :NSProxy, :NSServiceViewControllerUnifyingProxy, :NSVB_TargetedProxy, :NSVB_ViewServiceImplicitAnimationDecodingProxy,… # a quite lengthy list
The list which we just received shows all the constants (including classes) which are defined. As we are looking for cmark we will hit CMD+F
to search for cmark
and find … Nothing! Nothing? Yes, indeed the cmark library does not add any classes which are globally available. This is bad luck for us, but we still have got another trick up our sleeves.
Remember the vendor/cmark/cmark.h
header file which we copied above? We will now look into this file in order to find any methods which the library exposes.
After some search you will probably find the file src/cmark.h
. Well I am not a C expert and I never learned the language, but line 28 looks like a pretty straight method definition:
char *cmark_markdown_to_html(const char *text, size_t len, int options);
So this tells us, that:
- this method returns a char datatype
- the method name is
cmark_markdown_to_html
- first argument is the markdown text
- second argument is the length of the text, i.e. number of characters
- third argument provides additional options which we currently don’t know about
This is enough information for us to play around a little bit. Let’s edit the applications delegate class. First we will add a private method which returns a basic markdown string as a quick and dirty test:
private
def mdstring
"# Hello world\nThis is some **fancy** markdown."
end
end # end class AppDelegate
Next we try to parse the markdown string in a new method. We are using the cmark method we found out about above:
def parse_markdown
html_string = cmark_markdown_to_html(mdstring, mdstring.length, 0)
puts "The html is: #{html_string}"
end
So what we do is, call the cmark_markdown_to_html
method with the string from the mdstring
method, the length of the string and 0 for the integer option value, which we don’t know right now. Then we output the result via puts. We must also make sure to call this method by adding a method-call to the applicationDidFinishLaunching
method of the AppDelegate class:
def applicationDidFinishLaunching(notification)
buildMenu
buildWindow
parse_markdown # call the parse function
end
Now when we build and run the app the following should happen:
$ bundle execute rake
…
output: '<h1>hello world</h1>'
Hooray we just rendered html from a markdown string! Now you can go ahead and build something useful.
Wrap-Up
So let’s have a look once again on what we just did. First we created our Rubymotion project. Then we downloaded and built the cmark library. Next we had to add the library to our Rubymotion project. It was very important to also add the cmark.h
header file so that the messages are also discovered by Rubymotion. Then we edited the Rakefile to include cmark. Afterward we applied a few tricks to find out about how to call the cmark library from Rubymotion code. After we had found out how to do that, we successfully rendered a Markdown string and that’s it!
If you liked the tutorial or have any questions on it contact me via mail or also join the Rubymotion slack channel here: https://motioneers.herokuapp.com. Also check out my apps Docxtor and Gapped. Both were built using Rubymotion.