Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add gumath ruby wrapper. #23

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,8 @@ build/
doc/build/
*.egg-info
__pycache__

# Ruby generated build files
ruby/Gemfile.lock
*.gem
ruby/tmp/
61 changes: 61 additions & 0 deletions ruby/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Details

## Functioning of libgumath

Gumath is a wrapper over libgumath. The docs for libgumath can be found [here](https://xnd.readthedocs.io/en/latest/libgumath/index.html).

It allows users to write [multiple dispatch](https://en.wikipedia.org/wiki/Multiple_dispatch) functions for XND containers.
Multiple dispatch can be thought of as the concept behind function overloading in
languages like C++. However the major difference is that the function call is made
according to the run time types of the arguments in case of multiple dispatch whereas
in overloading it is determined at compile-time and the types of arguments determine
the 'signature' of the function. [This answer](https://cs.stackexchange.com/questions/4660/difference-between-multimethods-and-overloading) sheds some light on the differences
between the two. Here's [another link](http://wiki.c2.com/?MultiMethods) on the same topic.

This functionality is absent in Ruby because in Ruby we don't care about the type of
the arguments of a function when calling it (aka duck typing).

Gumath functions (or 'kernels') are functions that can accept XND objects of multiple
data types and compute the result depending on what kind of data type is sent in. The
data type is known only at run time (like determining whether a container is of type
float32 or int etc.) and the same function can handle all sorts of data types.

Libgumath stores functions in a look-up table. Each Ruby module containing functions
should have its own look-up table. A prototype of adding gumath kernels to a Ruby module
`Gumath::Functions` can be found in the functions.c file in this repo.

An XND kernel has the following function signature:
```
typedef int (* gm_xnd_kernel_t)(xnd_t stack[], ndt_context_t *ctx);
```

## Interfacing libgumath with Ruby

Unlike in Python, Ruby functions are not first-class objects and therefore we cannot create
a 'callable' object like we can in Python. Since libgumath kernels are run-time constructs
it is difficult to dynamically add these functions to a Ruby module at runtime since unlike
Ruby, C cannot generate new functions at run-time.

However, Ruby does have support for lambdas and we can store the function implementations
inside lambdas. These lambdas are stored in module variables that are of the same name
as the function name that they implement. When the user calls the lambda with the `call` method
or `.` syntax, the lambda will be passed the argument (after some conversion) and will
return the result given by the gumath kernel.

Another approach is to have a Hash of methods and their corresponding implementations
stored inside each module. Whenever a method is called on the module, it will routed to
the method_missing in the class, which will lookup the hash and call the appropriate
lambda. This seems like the better approach since the user no longer needs to know that
the functions that they're calling inside the module are in fact lambdas.

So each module that contains gumath methods should have a hash called `gumath_functions`
that stores function names as keys (as symbols) and the corresponding values as objects
of type `GufuncObject`. This `GufuncObject` will have a method `call` defined on it that
will be called by the `method_missing` when that object is called. The only extra overhead
will be that of a Hash lookup (O(1) time) and calling an Ruby method `call`.

## Calling gumath kernels

Gumath kernels can be called by using the `gm_apply` function and passing a pointer to
the function along with its arguments. Its multi-threaded counterpart, `gm_apply_thread`
can also be used for this purpose.
2 changes: 2 additions & 0 deletions ruby/Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
source 'https://rubygems.org'
gemspec
Empty file added ruby/History.md
Empty file.
5 changes: 5 additions & 0 deletions ruby/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Introduction

gumath is a library for computational kernels that target XND containers.


105 changes: 105 additions & 0 deletions ruby/Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
require 'rake'
require 'rake/extensiontask'
require 'rake/testtask'
require 'bundler/gem_tasks'
require 'fileutils'
require 'gumath/version.rb'

gemspec = eval(IO.read('gumath.gemspec'))

ext_name = "ruby_gumath"
Rake::ExtensionTask.new(ext_name, gemspec) do |ext|
ext.ext_dir = "ext/#{ext_name}"
ext.source_pattern = "**/*.{c,h}"
end

def run *cmd
sh(cmd.join(" "))
end

task :console do
cmd = ['irb', "-r './lib/gumath.rb'"]
run(*cmd)
end

task :pry do
cmd = ['pry', "-r './lib/gumath.rb'"]
run(*cmd)
end

Rake::TestTask.new(:test) do |t|
t.libs << "test"
t.libs << "lib"
t.test_files = FileList['test/**/test_*.rb']
end

BASEDIR = Pathname( __FILE__ ).dirname.relative_path_from( Pathname.pwd )
TESTDIR = BASEDIR + 'test'

VALGRIND_OPTIONS = [
"--tool=memcheck",
#"--leak-check=yes",
"--num-callers=15",
#"--error-limit=no",
"--partial-loads-ok=yes",
"--undef-value-errors=no" #,
#"--dsymutil=yes"
]

VALGRIND_MEMORYFILL_OPTIONS = [
"--freelist-vol=100000000",
"--malloc-fill=6D",
"--free-fill=66 ",
]

# partial-loads-ok and undef-value-errors necessary to ignore
# spurious (and eminently ignorable) warnings from the ruby
# interpreter

desc "Run specs under Valgrind."
task :valgrind => [ :compile ] do |task|
cmd = [ 'valgrind' ] + VALGRIND_OPTIONS
cmd += [" rake test "]
run( *cmd )
end

LEAKCHECK_CMD = [ 'ruby', '-Ilib:ext', "#{TESTDIR}/leakcheck.rb" ]

desc "Run leakcheck script."
task :leakcheck => [ :compile ] do |task|
cmd = [ 'valgrind' ] + VALGRIND_OPTIONS
cmd += LEAKCHECK_CMD
run( *cmd )
end

task :clobber do |task|
[
"ext/#{ext_name}/include",
"ext/#{ext_name}/share",
"ext/#{ext_name}/lib",
].each do |f|
puts "deleting folder #{f}..."
FileUtils.rm_rf(f)
end

Dir.chdir("ext/#{ext_name}/gumath/libgumath/") do
system("make clean")
end
end

task :develop do
ext_gumath = "ext/ruby_gumath/gumath"
puts "deleting previously created #{ext_gumath} directory..."
FileUtils.rm_rf(ext_gumath)
Dir.mkdir(ext_gumath)

puts "cloning gumath repo into ext/ folder..."
system("git clone https://github.com/plures/gumath #{ext_gumath}")

Dir.chdir(ext_gumath) do
system("git checkout #{Gumath::COMMIT}")
end

puts "building gem with rake build..."
system("rake build")
end
126 changes: 126 additions & 0 deletions ruby/ext/ruby_gumath/examples.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/* BSD 3-Clause License
*
* Copyright (c) 2018, Quansight and Sameer Deshmukh
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include <ruby.h>
#include "ndtypes.h"
#include "ruby_ndtypes.h"
#include "gumath.h"
#include "gufunc_object.h"
#include "ruby_gumath.h"
#include "util.h"

/****************************************************************************/
/* Static globals */
/****************************************************************************/

/* Function table */
static gm_tbl_t *table = NULL;
static VALUE mGumath_Examples;
static int initialized = 0;

/****************************************************************************/
/* Singleton methods */
/****************************************************************************/
static VALUE
mGumath_Examples_s_method_missing(int argc, VALUE *argv, VALUE module)
{
VALUE method_name = argv[0];
VALUE method_hash = rb_ivar_get(module, GUMATH_FUNCTION_HASH);
VALUE gumath_method = rb_hash_aref(method_hash, method_name);
int is_present = RTEST(gumath_method);

if (is_present) {
rb_funcall2(gumath_method, rb_intern("call"), argc-1, &argv[1]);
}
else {
VALUE str = rb_funcall(method_name, rb_intern("to_s"), 0, NULL);
rb_raise(rb_eNoMethodError, "Method %s not present in this gumath module.",
StringValueCStr(str));
}
}

void
Init_gumath_examples(void)
{
NDT_STATIC_CONTEXT(ctx);

if (!initialized) {
if (!ndt_exists()) {
rb_raise(rb_eLoadError, "NDT is needed for making gumath work.");
}

if (!xnd_exists()) {
rb_raise(rb_eLoadError, "XND is needed for making gumath work.");
}

table = gm_tbl_new(&ctx);
if (table == NULL) {
seterr(&ctx);
raise_error();
}

/* load custom examples */
if (gm_init_example_kernels(table, &ctx) < 0) {
seterr(&ctx);
raise_error();
}

/* extending examples */
if (gm_init_graph_kernels(table, &ctx) < 0) {
seterr(&ctx);
raise_error();
}

#ifndef _MSC_VER
if (gm_init_quaternion_kernels(table, &ctx) < 0) {
seterr(&ctx);
raise_error();
}
#endif
if (gm_init_pdist_kernels(table, &ctx) < 0) {
seterr(&ctx);
raise_error();
}

initialized = 1;
}

mGumath_Examples = rb_define_module_under(cGumath, "Examples");
rb_ivar_set(mGumath_Examples, GUMATH_FUNCTION_HASH, rb_hash_new());

if (rb_gumath_add_functions(mGumath_Examples, table) < 0) {
mGumath_Examples = Qundef;
rb_raise(rb_eLoadError, "failed to load functions into Gumath::Examples module.");
}

/* Singleton methods */
rb_define_singleton_method(mGumath_Examples, "method_missing",
mGumath_Examples_s_method_missing, -1);
}
Loading