-
Notifications
You must be signed in to change notification settings - Fork 19
Creating a Class for Shoes in C
[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.
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.
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)
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.