FFI to SDK

Comparing

1
2
3
4
5
6
7
8
void drawLine(CGFloat x0, CGFloat y0, CGFloat x1, CGFloat y1)
{
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextBeginPath(c);
CGContextMoveToPoint(c, x0, y0);
CGContextAddLineToPoint(c, x1, y1);
CGContextStrokePath(c);
}

to

1
2
3
4
5
6
7
draw_line :: CGFloat -> CGFloat -> CGFloat -> CGFloat -> IO ()
draw_line x y x' y' = do
c <- _UIGraphicsGetCurrentContext
_CGContextBeginPath c
_CGContextMoveToPoint c x y
_CGContextAddLineToPoint c x' y'
_CGContextStrokePath c

, the Haskell one isn't terribly different from the original Objective-C code. By building FFI agains the iPhone SDK, Objective-C code can be kept both minimal and functional.

I really began to feel that Cocoa is indeed very nicely designed, and those design patterns for Objective-C are just about right for building GUI.

Custom Font

It turned out to be not so trivial to use a custom font. I experimented a lot, and finally reached this solution.

It requires 2 steps:

1. Load the ttf

1
2
3
4
5
6
7
NSString *fontPath = [[NSBundle mainBundle] 
pathForResource:@"#{font_file_name}" ofType:@"ttf"];

CGDataProviderRef fontDataProvider =
CGDataProviderCreateWithFilename([fontPath UTF8String]);

ref = CGFontCreateWithDataProvider(fontDataProvider);

2. Draw glyphs

1
2
3
4
CGContextSetFont(_context, ref);
CGContextSetFontSize(_context, 80);
CGGlyph i = 40;
CGContextShowGlyphsAtPoint(_context, x, y, &i, 1);

It helpers to invert the transformation matrix before drawing, as Apple fucked up the view coordinates in UIKit.

1
2
3
CGRect viewBounds = self.bounds;
CGContextTranslateCTM(_context, 0, viewBounds.size.height);
CGContextScaleCTM(_context, 1, -1);
  • 1.png
Quantify

There were many things to be done to bootstrap the project, I chose to try to quantify the drawing. It turned out to be a strait forward task.

Nothing had to be changed in the Objective-C part, yet code size grew dramatically, since the initial set up using mtl did contain some boilerplates.

There was a problem though: I could not figure out how to use ghci to inspect Main.hs, as Main.hs requires linking to C.

It also turns out that remote logging is not necessary, the XCode debug panel will show stderr stream.

  • 1.png
Monad on iPhone

This concept code includes

  • 150 lines of Haskell
  • 50 lines of Objective-C

Objective-C only handles the hardware interface, no logic or state are involved. Haskell does the app logic including calling drawing routings.

  • iPhone GHC demo.png
Logging

I saw the logging code commented in the sample project, seems the only thing missing is a simple remote logging server.

RemoteLog.hs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import Network
import Prelude ()
import Control.Monad
import System.IO
import MPS.Env

main :: IO ()
main = do

let port = 4242
puts = putStrLn

puts - "listening on port: " ++ port.show

socket <- listenOn - PortNumber port

forever - do
(h, _, _) <- accept socket

hGetLine h >>= puts
hClose h

Main.hs

1
2
3
4
5
6
7
8
9
10
openLogger :: IO Handle
openLogger = do
log <- connectTo "127.0.0.1" (PortNumber 4242)
hSetBuffering log LineBuffering
return log

log :: String -> IO ()
log x = do
hLog <- openLogger
hPutStrLn hLog x
1
2
main = do
log "init main ..."
Calling Haskell (1)

This post is going to be very boring.

BTW, all the info is contained inside the sample projects in GHC-iPhone release, I'm just documenting it to remember it better.

There's no wrapper to use for the iPhone SDK, so the programer will essentially be building a subset of Objective-C wrapper in C and manually creating tunnels to call these C functions in Haskell.

The task can be broken down into 3 Big steps:

1. Create

  • wrap an Objective-C method in C
  • import the C function into Haskell through FFI
  • create a function that utilize this imported function in Haskell

2. Export

  • create an empty function pointer in C
  • create a setter function to initialize this function pointer in C
  • import the setter function into Haskell
  • Use the setter to initialize a Haskell function

3. Use

  • somewhere in Objective-C, call into Haskell through this initialized function

Step 2 contains some boilerplates: the function pointer and initializer, so I wrote

OMG

my first C Macro! ><

1
2
3
4
5
6
7
8
9
10
/* 
H(draw_rect) ->

void (*draw_rect)(void);
void set_draw_rect(void (*cb)() {
draw_rect = cb;
}
*/

#define H(f) \ void (*f)(void);\ void set_##f(void (*cb)()) {\ f = cb;\ }

So my first experiment allows Haskell to draw a line in the view, here's the relavent code:

ScoreView.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@implementation ScoreView

void drawLine(double x0, double y0, double x1, double y1)
{
CGContextRef _context = UIGraphicsGetCurrentContext();
CGContextBeginPath(_context);
CGContextMoveToPoint(_context, x0, y0);
CGContextAddLineToPoint(_context, x1, y1);
CGContextStrokePath(_context);
}

H(draw_rect)

- (void)drawRect:(CGRect)rect {
if (draw_rect != nil) draw_rect();
}

@end

Main.hs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
foreign import ccall safe "drawLine" drawLine :: CDouble -> CDouble -> CDouble -> CDouble -> IO ()
foreign import ccall safe "wrapper" mkPlain :: IO () -> IO (FunPtr (IO ()))
foreign import ccall safe "set_draw_rect" set_draw_rect :: FunPtr (IO ()) -> IO ()


main = do
draw_rect <- mkPlain $ drawLine 0 0 220 220
set_draw_rect draw_rect

kon

free draw_rect

where
free = freeHaskellFunPtr
K-on start

I've decided that the next project will be a music composition software targeting the iPhone platform.

If I fail eventually, I shall fail while doing what I still like.

Anyway, because the GHC-iphone port is so advance ( there are even hand written assembly code for the linker), there's no hope for me to take it over if it no longer works with future iPhone OS. But since GHC will have an LLVM backend, hopefully I can at least still call into haskell by using that.

I've been wanting to do this for a long time, and this will be my most complex project.

code name: K-On!.