Skip to content

Creating a Class for Shoes in C

Cecil Coupe edited this page Nov 16, 2015 · 6 revisions

[Update 2015 Nov 15] Creating a new class is not the same as creating a new "native" widget. The code below describes a failed attempt to get svg into Shoes. There are lessons in that failure. How to wire in a C class/method to Canvas still applies.

In Shoes 3.3.0 we wanted to create a class to represent Librsvg's RsvgHandle. This class doesn't do any drawing it just creates that handles new(file), new(string) and provides some getters and setters to manipulate the RsvgHandle in the object. We may have to deal with ruby gc or maybe not.

First we need a Class name. 'svg' would not be good here because we might want that much later when get we around to drawing. 'svghandle' will do for now. Then we need a class name for the C code - that will be cSvgHandle which needs to entry in shoes/ruby.h extern VALUE cSvgHandle; And we'll match it with a variable declared in shoes/ruby.c near the top of the file with VALUE cSvgHandle;

Now we need to modify shoes/ruby.c to create the Ruby object in the Shoes hierarchy of classes. It's not going under Shoes.app (too much confusion in there already). We'll use cTypes even though this class is not drawable. We'll use Timer/TimerBas as a prototype for our SvgHandle. So Way down in shoes/ruby.c around line: 5002 we add

  cSvgHandle    = rb_define_class_under(cTypes, "SvgHandle", rb_cObject);
  rb_define_alloc_func(cSvgHandle, shoes_svghandle_alloc);
  rb_define_method(cSvgHandle, "close", CASTHOOK(shoes_svghandle_close),0);

That's our svghandle.close() method but where is the new()? This confuses everyone, every time. In ruby.c:4553 or so is a CANVAS_DEFS macro expansion and as the comment says the macro is in ruby.h - we need our svghandle in there. Here is ours (line 253) - right after timer (our prototype)

f("+svghandle", svghandle, -1); \

We also need the function/method prototypes in canvas.h because just about everything in shoes has a canvas and we are no exception. So in canvas.h:578 we add

VALUE shoes_svghandle_new(VALUE, VALUE);
VALUE shoes_svghandle_alloc(VALUE);
VALUE shoes_svghandle_close(VALUE);

Now we'll write some skeleton code for our functions. Just enough to compile and have something we can put a breakpoint on in gdb (lldb for OSX)

// new in 3.3.0

VALUE shoes_svghandle_new(VALUE klass, VALUE arg)
{
  return Qnil;
}
VALUE
shoes_svghandle_alloc(VALUE klass)
{
  return Qnil;
}

VALUE
shoes_svghandle_close(VALUE self)
{
  return Qnil;
}

Wait! There's more! Because svghandle is a canvas method we need a function in canvas.c and canvas.h that matches the macro generated name. Below is the shoes_canvas_timer code rewritten to be shoes_canvas_svghandle. in canvas.h:488 add

VALUE shoes_canvas_svghandle(int, VALUE *, VALUE);

In canvas.c:

VALUE
shoes_canvas_svghandle(int argc, VALUE *argv, VALUE self)
{
  rb_arg_list args;
  VALUE handle;
  SETUP();

  rb_parse_args(argc, argv, "|I&", &args);
  handle = shoes_svghandle_new(cSvgHandle, self);
  rb_ary_push(canvas->app->extras, handle);
  return handle;
}

We'll need a test script. (bugs/bug054.rb)

Shoes.app do
  svgh = svghandle.new("string");
end

Time to compile and build shoes and fix any typos. There will something to fix.

Once that's working lets fire up gdb.

gdb dist/shoes on Linux Loose Shoes which is really what you want to run if you're doing this.

$ gdb dist/shoes
GNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.2) 7.7.1
....
Reading symbols from dist/shoes...done.
(gdb) br shoes_canvas_svghandle
Breakpoint 1 at 0x426bd7: file shoes/canvas.c, line 786.
(gdb) r bugs/bug054.rb 
Starting program: /home/ccoupe/Projects/shoes3/dist/shoes bugs/bug054.rb
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7ffff7ff7700 (LWP 6014)]
[New Thread 0x7fffedffa700 (LWP 6015)]

Breakpoint 1, shoes_canvas_svghandle (argc=0, argv=0x7ffff7eb1088, 
    self=6947520) at shoes/canvas.c:786
786	  SETUP();
(gdb) 

We got it! I know the arg parsing in shoes_canvas_svghandle() is not correct and we haven't written the code for new or alloc or close.

Pushing further.

I'm not going to show the code for all the steps below because they get more and more task specific. In this case we are encapulating a C library and there is no need to call gtk/osx code which involves some more files and naming themes.

We have to think about ruby gc and I think the RsvgHandle is reference counted so we want to do something appropriate there. We'll have to adjust the functions and declares to match what args do make sense once we know what they are. Then we can really test what new() and close() do and create the methods that work on the svghandle object.

Since I expect this is going to be a lot of code, I moved the new,alloc and close methods to a new file svghandle.c. We have to change the env.rb to get the -I for headers and the -L for libaries. One platform at a time. PITA.

One thing left out is that we need a struct shoes_svghandle to hold that RsvgHandle plus whatever fields we need associated with it. That's done in canvas.h around line 273 and we're going to need a new field in canvas - currently we're adding to where timers are stored which might not be a good idea. Pretty sure it's not a good idea because timers are attached to app->extras and we want svghandle per window/canvas not the app level.

Reluctantly, I'm going to add line 324 to canvas.h

  shoes_svghandle *svg;     // 3.3.0  an svg on this canvas

Purists might suggest that Contents is the proper place. They might be correct but I'm going with gut feel. (note that cairo_t *cr is stored there - we want that when we get to drawing, if we are get that far).

Now we need to actually load an file.svg and see who segfaults. Modify bugs/bug054.rb so it has a file argument which we'll try to load into our svghandle object

# simple test of svghandle class - full path name is important
Shoes.app do
  fpath = "/home/ccoupe/Projects/shoes3/icon/brownshoes.svg"
  svgh = svghandle({:filename => fpath})
  svgh.close
  fl = File.open(fpath,"r");
  bigstring = fl.read
  fl.close
  para "SVG is #{bigstring.length} long"
  sgvg2 = svghandle({:from_string => bigstring})
  svg2.draw
  svg2.close
end

Before we can debug anything, we need to parse the ruby arguments given to shoes_canvas_svghandle (in canvas.c). We want a hash {:filename => "file-pathname"} or {:from_string => svg-xml-string} As currently written we'll pass a non-nil arg in path or string arguments to shoes_svghandle_new. That might change because I learn things after I start coding.

rb_parse_args is a function in Shoes - it's not in the ruby embedding api, defined in shoes/ruby.c. Like or dislike doesn't matter. Work with what you have. I choose to not use it.

NOTE: rb_parse_args deserves a wiki post of it's own in this section. I'm probably doing it wrong.

Note

This note can not describe all the iterative fooling about in the C code. It probably won't work if you cut and paste from this note. (although you'll get close when the C compiler stops complaining)

Lets try drawing the loaded svg on screen in Shoes.

since this mostly an experiment or tutorial, I'm going to put the .draw method and code on the svghandle object and not on canvas. We have a link to the canvas it was created on. In ruby.c:4988 or so

  rb_define_method(cSvgHandle, "draw", CASTHOOK(shoes_svghandle_draw), 0);

In canvas.h:591 or so add

VALUE shoes_svghandle_draw(VALUE);

We need to add shoes_svghandle_draw() to shoes/svghandle.c That function doesn't have to process any args. It gets called. It fails to do the right thing Shoes wise (and I'm not happy with how it looks Shoes-wise) It's only an experiment and this article is about how to create a Shoes class from C.

Clone this wiki locally