Showing posts with label jruby. Show all posts
Showing posts with label jruby. Show all posts

Wednesday, June 13, 2007

Hacking JRuby: More on Method Arguments

I closed my last post with this observation:
"...JRuby does not have an equivalent for rb_scan_args(), or at least not one that is called on a per-method basis."

In the comments, Ola Bini -- ThoughtWorker and JRuby committer -- graciously pointed out that there are, in fact, two static methods that we can use to simulate the function of rb_scan_args(), both found in the org.jruby.runtime.Arity class: checkArgumentCount(), and scanArgs().

checkArgumentCount() does just what it says. You pass it an array of argument values, along with the minimum and maximum number of all arguments, and it verifies that the actual number of arguments falls within that range (inclusive). So this provides some basic sanity checking of the number of arguments passed to the method you're implementing. If the number of arguments is valid, checkArgumentCount() returns the actual number of arguments passed. I guess you could argue that this isn't very useful, since you already know the number of arguments you expect, as well as the number given to you. But it is used in a number of places in the JRuby source code.

scanArgs() will do a little more work for you. It will do the same sanity checking (it actually calls checkArgumentCount() to do so), although you specify the numbers slightly differently, passing the number of required arguments along with the number of optional arguments. If the actual number of arguments passed is valid, scanArgs() then creates a new array of length required + optional, copies the values of any arguments passed in (all required arguments plus any provided optional arguments), sets the values of any non-provided optional arguments to nil, and returns the new array. This actually is useful, as it transforms a variable-length array of arguments into one of fixed length, where the fixed length is always equal to the maximum number of arguments your method will accept.

As Ola points out, this still is not quite the equivalent of MRI rb_scan_args(), in that it doesn't handle "rest" or "block" arguments. But it does offload from us some of the burden of handling variable length argument lists.

I do plan to use scanArgs() to finish up my implementation of BigDecimal.mode(), but in order to so meaningfully I'm also going to have to change mode()'s method definition from taking a fixed number of arguments to taking a variable number of arguments (the first argument is required, the second is optional). That's going to involve digging into JRuby's mechanism for defining Ruby methods, which I've researched and found to be pretty cool, but it's going to take another few blog posts to sort out. Stay tuned...

Tuesday, June 12, 2007

Hacking JRuby: BigDecimal and Ruby Internals

I've submitted another patch for JRuby (viewable here), to implement the BigDecimal.mode() class method. In doing so, I learned quite a bit about JRuby's implementation strategy, as well as the internals of the C source code for MRI (Matz's Ruby Implementation) Ruby.

BigDecimal.mode() explained

BigDecimal.mode() is a funky little method in the BigDecimal module, which is not part of the core API but part of the standard library that ships with Ruby. It's kind of multi-variate -- what it does, exactly, depends on how it's called.

The first parameter to BigDecimal.mode() is required, and it must be a Fixnum representing either the constant BigDecimal::ROUNDING_MODE or the exception mode to be set (more on that later). If it's BigDecimal::ROUNDING_MODE and there is no second argument, then mode() just returns the current rounding mode. If a second argument is present, it must also be a Fixnum, and it must equate to one of the seven rounding modes Ruby recognizes (e.g., BigDecimal::ROUND_UP, BigDecimal::ROUND_FLOOR, etc.). In this case, mode() sets the rounding mode (for all BigDecimals, remember, since this is a class method) to the value of the second argument.

If the first argument is a Fixnum that is not equal to BigDecimal::ROUNDING_MODE, then it is expected to have one of its bits set to correspond to one of the known exception modes (e.g., BigDecimal::EXCEPTION_INFINITY). Again, if there is no second argument, mode() simply reports the current exception mode(s) (each bit in the returned value corresponds to a single exception mode set). If there is a second argument, it must be one of 'true' or 'false'. If 'true', mode() sets the mode passed in the first argument. If 'false', mode() unsets (i.e., turns off) the mode passed in the first argument.

Simple, huh?

Not So Fast...

When I picked up this task, mode() was just a default stub that printed a message to the console and returned nil. Not a lot to go on there. So I turned to the MRI source code to figure out just what it was supposed to do.

Introducing: rb_scan_args()

One of the first things MRI does (in a lot of methods, as it turns out) is to call the function rb_scan_args()), which is implemented in the file class.c with the following signature:
int rb_scan_args(int argc, const VALUE *argv, const char *fmt, ...)
It takes the number of arguments passed, a pointer to a structure containing the values of those arguments, a format string of some sort, and...some other stuff. The number and values of the arguments are self-explanatory, but the format string and the trailing "other stuff" are decidedly not, so let's take a look at them.

The format string consists, minimally, of two digits. The first digit is the number of required arguments, the second is the number of optional arguments. rb_scan_args parses the format string to find these numbers, then it walks the list of argument values and stuffs each value into its corresponding reference (which is what the "other stuff" in the signature actually is: a group of references to store the values of the arguments in).

For example, BigDecimal.mode() makes this call to rb_scan_args:
if(rb_scan_args(argc,argv,"11",&which,&val)==1) val = Qnil;
In English:

  • get one required argument and store its value in the variable which

  • get the optional second argument if it exists and put its value in val

  • if rb_scan_args returned 1 (i.e., only one argument was provided), then set the value of the optional argument to its default of nil

So this is how MRI Ruby (as implemented in C) handles variable/optional arguments in a general way. There's more to it, of course, including an astonishing bit of hackery with C macros that actually implements putting the argument values in the right place for return. But I won't go into that until I understand it better. Also, the format string allows for the Ruby constructs of "rest args" (indicated by an '*' in the format string) and finally a "block arg" (indicated by an '&').

Meanwhile, Back in JRuby...

This has gone on a bit long, so I'll just close by saying that JRuby does not have an equivalent for rb_scan_args(), or at least not one that is called on a per-method basis. The runtime is responsible for bundling arguments and calling the appropriate Java method based on the number of arguments actually present. This causes a bit of a problem right now for class methods that take optional arguments (as BigDecimal.mode() does), but that's a subject for another post.

Tuesday, May 29, 2007

Hacking JRuby

I recently got to contribute to the JRuby project -- issue #JRUBY-914: "JRuby's BigDecimal needs to round". In doing so, I got to learn a bit about Java's BigDecimal -- particularly how it differs from Java 1.4 to Java 5, implement a bit of code in the JRuby codebase, and test my ideas conveniently and interactively using Jython. Not a bad way to spend a couple hours hacking and contributing to the community.

In a nutshell, java.math.BigDecimal allows you to specify a scale, which is the number of digits to the right of the decimal place. So a BigDecimal with an unscaledValue of 1 and a scale of 1 would evaluate to 0.1. Java 5 allows the scale to take a negative value. So a BigDecimal with an unscaledValue of 1 and a scale of -1 would evaluate to 10. This is significant for rounding because it allows you to round a number to any digit, right or left of the decimal. But Java 1.4 does not allow a negative scale, so we need another approach to allow the same behavior on both versions.

My solution was to use the movePointLeft() and movePointRight() methods of the BigDecimal class to achieve the same result. It turns out those methods do permit a negative offset, so moving the decimal point "right" by a negative number actually moves it to the left, and vice versa. This turns out to be all we need to achieve the desired behavior: moving the decimal point to the right scale times positions the digit to be rounded to just to the left of the decimal point. We can then round normally, using whichever Java rounding mode suits our needs. Then we shift the decimal point left scale times to restore the (now rounded) number.

Note that this algorithm works for both negative and positive values of scale. It's a little bit of overkill when scale is non-negative, though, so I just implemented it for the interesting case of scale < 0.