From 252d606c09831e76edea82dbb24094528da5741f Mon Sep 17 00:00:00 2001 From: Patrick Mahoney Date: Sun, 15 Jan 2012 08:02:53 -0600 Subject: [PATCH] Add FFI wrapper around some of the Mach API (in Mac OS X). --- lib/mach.rb | 2 + lib/mach/bootstrap.rb | 35 +++++ lib/mach/error.rb | 52 ++++++++ lib/mach/functions.rb | 252 ++++++++++++++++++++++++++++++++++++ lib/mach/port.rb | 47 +++++++ lib/mach/semaphore.rb | 58 +++++++++ lib/mach/task.rb | 40 ++++++ spec/mach/port_spec.rb | 21 +++ spec/mach/semaphore_spec.rb | 34 +++++ spec/mach/task_spec.rb | 31 +++++ 10 files changed, 572 insertions(+) create mode 100644 lib/mach.rb create mode 100644 lib/mach/bootstrap.rb create mode 100644 lib/mach/error.rb create mode 100644 lib/mach/functions.rb create mode 100644 lib/mach/port.rb create mode 100644 lib/mach/semaphore.rb create mode 100644 lib/mach/task.rb create mode 100644 spec/mach/port_spec.rb create mode 100644 spec/mach/semaphore_spec.rb create mode 100644 spec/mach/task_spec.rb diff --git a/lib/mach.rb b/lib/mach.rb new file mode 100644 index 0000000..b98cbf1 --- /dev/null +++ b/lib/mach.rb @@ -0,0 +1,2 @@ +module Mach +end diff --git a/lib/mach/bootstrap.rb b/lib/mach/bootstrap.rb new file mode 100644 index 0000000..0696e2e --- /dev/null +++ b/lib/mach/bootstrap.rb @@ -0,0 +1,35 @@ +require 'ffi' + +require 'mach' +require 'mach/types' + +module Mach + module Bootstrap + extend FFI::Library + include Types + + ffi_lib 'c' + + attach_variable :port, :bootstrap_port, :mach_port_t + + attach_function(:bootstrap_strerror, + [:kern_return_t], + :pointer) + + attach_function(:register, + :bootstrap_register, + [:mach_port_t, :name_t, :mach_port_t], + :kern_return_t) + + error_check :register + + # NOTE: api does not say this string must be freed; assuming it + # does not + # + # @return [String] the error string or nil + def self.strerror(errno) + ptr = bootstrap_strerror(errno) + ptr.null? ? nil : ptr.read_string() + end + end +end diff --git a/lib/mach/error.rb b/lib/mach/error.rb new file mode 100644 index 0000000..34c8104 --- /dev/null +++ b/lib/mach/error.rb @@ -0,0 +1,52 @@ +require 'ffi' + +require 'mach/functions' + +module Mach + class Error < StandardError + class INVALID_ADDRESS < Error; end + class PROTECTION_FAILURE < Error; end + class NO_SPACE < Error; end + class INVALID_ARGUMENT < Error; end + class INVALID_NAME < Error; end + class FAILURE < Error; end + + include Functions + + def self.new(msg, errno) + klass = case errno + when 1; then INVALID_ADDRESS + when 2; then PROTECTION_FAILURE + when 3; then NO_SPACE + when 4; then INVALID_ARGUMENT + when 5; then FAILURE + when 15; then INVALID_NAME + else FAILURE + end + + e = klass.allocate + e.send(:initialize, msg, errno) + e + end + + attr_reader :errno + + def initialize(msg, errno) + super(msg) + @errno = errno + end + + def to_s + "#{super}: #{error_string(errno)}" + end + + protected + + # NOTE: api does not say this string must be freed; assuming it + # does not + def error_string(errno) + ptr = mach_error_string(errno) + ptr.null? ? nil : ptr.read_string() + end + end +end diff --git a/lib/mach/functions.rb b/lib/mach/functions.rb new file mode 100644 index 0000000..aae5bb0 --- /dev/null +++ b/lib/mach/functions.rb @@ -0,0 +1,252 @@ +module Mach + # FFI wrapper around a subset of the Mach API (likely Mac OS X + # specific). + module Functions + extend FFI::Library + + ffi_lib 'c' + + typedef :__darwin_mach_port_t, :mach_port_t + typedef :__darwin_natural_t, :natural_t + + typedef :mach_port_t, :task_t + typedef :mach_port_t, :ipc_space_t + typedef :mach_port_t, :semaphore_t + typedef :pointer, :mach_port_pointer_t + + typedef :natural_t, :mach_port_name_t + typedef :natural_t, :mach_port_right_t # MachPortRight + typedef :pointer, :mach_port_name_array_t + typedef :pointer, :mach_port_name_pointer_t + + typedef :uint, :mach_msg_type_name_t + + typedef :int, :kern_return_t # true for 64 bit?? + typedef :int, :mach_error_t + typedef :int, :sync_policy_t # SyncPolicy + + typedef :string, :name_t + + SyncPolicy = enum( :fifo, 0x0, + :fixed_priority, 0x1, + :reversed, 0x2, + :order_mask, 0x3, + :lifo, 0x0 | 0x2, # um... + :max, 0x7 ) + + MachPortRight = enum( :send, 0, + :receive, + :send_once, + :port_set, + :dead_name, + :labelh, + :number ) + + # port type + def self.pt(*syms) + acc = 0 + syms.each do |sym| + acc |= (1 << (MachPortRight[sym] + 16)) + end + acc + end + + MachPortType = + enum(:none, 0, + :send, pt(:send), + :receive, pt(:receive), + :send_once, pt(:send_once), + :port_set, pt(:port_set), + :dead_name, pt(:dead_name), + :labelh, pt(:labelh), + + :send_receive, pt(:send, :receive), + :send_rights, pt(:send, :send_once), + :port_rights, pt(:send, :send_once, :receive), + :port_or_dead, pt(:send, :send_once, :receive, :dead_name), + :all_rights, pt(:send, :send_once, :receive, :dead_name, :port_set)) + + MachMsgType = + enum( :move_receive, 16, # must hold receive rights + :move_send, # must hold send rights + :move_send_once, # must hold sendonce rights + :copy_send, # must hold send rights + :make_send, # must hold receive rights + :make_send_once, # must hold receive rights + :copy_receive ) # must hold receive rights + + MachSpecialPort = + enum( :kernel, 1, + :host, + :name, + :bootstrap ) + + class Timespec < FFI::ManagedStruct + layout(:tv_sec, :uint, + :tv_nsec, :int) + end + + KERN_SUCCESS = 0 + + # Replace methods in +syms+ with error checking wrappers that + # invoke the original method and raise a {SystemCallError}. + # + # The original method is invoked, and it's return value is passed + # to the block (or a default check). The block should return true + # if the return value indicates an error state. + def self.error_check(*syms, &is_err) + unless block_given? + is_err = lambda { |v| (v != KERN_SUCCESS) } + end + + syms.each do |sym| + method = self.method(sym) + + new_method_body = proc do |*args| + ret = method.call(*args) + if is_err.call(ret) + raise Error.new("error in #{sym}", ret) + else + ret + end + end + + define_singleton_method(sym, &new_method_body) + define_method(sym, &new_method_body) + end + end + + # Replace methods in +syms+ with error checking wrappers that + # invoke the original method and raise a {SystemCallError}. + # + # The original method is invoked, and it's return value is passed + # to the block (or a default check). The block should return true + # if the return value indicates an error state. + def self.error_check_bootstrap(*syms, &is_err) + unless block_given? + is_err = lambda { |v| (v != KERN_SUCCESS) } + end + + syms.each do |sym| + method = self.method(sym) + + new_method_body = proc do |*args| + ret = method.call(*args) + if is_err.call(ret) + ptr = bootstrap_strerror(ret) + msg = ptr.null? ? nil : ptr.read_string() + raise "error in #{sym}: #{msg}" + else + ret + end + end + + define_singleton_method(sym, &new_method_body) + define_method(sym, &new_method_body) + end + end + + # Attach a function as with +attach_function+, but check the + # return value and raise an exception on errors. + def self.attach_mach_function(sym, argtypes, rettype) + attach_function(sym, argtypes, rettype) + error_check(sym) + end + + def self.new_memory_pointer(type) + FFI::MemoryPointer.new(find_type(type)) + end + + def new_memory_pointer(type) + Mach::Functions.new_memory_pointer(type) + end + + attach_function :mach_task_self, [], :task_t + attach_function :mach_error_string, [:mach_error_t], :pointer + + ####################### + # Bootstrap functions # + ####################### + + attach_variable :bootstrap_port, :mach_port_t + + attach_function(:bootstrap_strerror, + [:kern_return_t], + :pointer) + + attach_function(:bootstrap_register, + [:mach_port_t, :name_t, :mach_port_t], + :kern_return_t) + error_check_bootstrap :bootstrap_register + + ################## + # Port functions # + ################## + + attach_mach_function(:mach_port_allocate, + [:ipc_space_t, + MachPortRight, + :mach_port_name_pointer_t], + :kern_return_t) + + attach_mach_function(:mach_port_destroy, + [:ipc_space_t, + :mach_port_name_t], + :kern_return_t) + + attach_mach_function(:mach_port_deallocate, + [:ipc_space_t, + :mach_port_name_t], + :kern_return_t) + + attach_mach_function(:mach_port_insert_right, + [:ipc_space_t, + :mach_port_name_t, + :mach_port_t, + MachMsgType], + :kern_return_t) + + ################## + # Task functions # + ################## + + attach_mach_function(:task_get_special_port, + [:task_t, + :int, + :mach_port_pointer_t], + :kern_return_t) + + attach_mach_function(:task_set_special_port, + [:task_t, + :int, + :mach_port_t], + :kern_return_t) + + ####################### + # Semaphore functions # + ####################### + + attach_mach_function(:semaphore_create, + [:task_t, :pointer, SyncPolicy, :int], + :kern_return_t) + attach_mach_function(:semaphore_destroy, + [:task_t, :semaphore_t], + :kern_return_t) + + attach_mach_function(:semaphore_signal, + [:semaphore_t], + :kern_return_t) + attach_mach_function(:semaphore_signal_all, + [:semaphore_t], + :kern_return_t) + attach_mach_function(:semaphore_wait, + [:semaphore_t], + :kern_return_t) + attach_mach_function(:semaphore_timedwait, + [:semaphore_t, Timespec.val], + :kern_return_t) + + end +end + +require 'mach/error' diff --git a/lib/mach/port.rb b/lib/mach/port.rb new file mode 100644 index 0000000..f1cae0b --- /dev/null +++ b/lib/mach/port.rb @@ -0,0 +1,47 @@ +require 'mach/functions' + +module Mach + class Port + include Functions + + attr_reader :ipc_space, :port + + # either initialize(port, opts) -or- initialize(opts) + def initialize(opts = {}, opts2 = {}) + if opts.kind_of? Hash + ipc_space = opts[:ipc_space] || mach_task_self + right = opts[:right] || :receive + + mem = new_memory_pointer(:mach_port_right_t) + mach_port_allocate(ipc_space, right, mem) + + @port = mem.get_uint(0) + @ipc_space = ipc_space + else + @port = opts + @ipc_space = opts2[:ipc_space] || mach_task_self + end + end + + def ==(other) + (port == other.port) && (ipc_space == other.ipc_space) + end + + def destroy(opts = {}) + ipc_space = opts[:ipc_space] || @ipc_space + mach_port_destroy(ipc_space, @port) + end + + def deallocate(opts = {}) + ipc_space = opts[:ipc_space] || @ipc_space + mach_port_deallocate(ipc_space, @port) + end + + def insert_right(msg_type, opts = {}) + ipc_space = opts[:ipc_space] || @ipc_space + port_name = opts[:port_name] || @port + + mach_port_insert_right(ipc_space, port_name, @port, msg_type) + end + end +end diff --git a/lib/mach/semaphore.rb b/lib/mach/semaphore.rb new file mode 100644 index 0000000..5a4c937 --- /dev/null +++ b/lib/mach/semaphore.rb @@ -0,0 +1,58 @@ +require 'mach/functions' +require 'mach/port' + +module Mach + class Semaphore < Port + include Functions + + # Create a new Semaphore. + # + # @param [Integer] value the initial value of the semaphore + # + # @param [Hash] opts + # + # @option opts [Integer] :task the Mach task that owns the + # semaphore (defaults to Mach.task_self) + # + # @options opts [Integer] :sync_policy the sync policy for this + # semaphore (defaults to SyncPolicy::FIFO) + # + # @return [Integer] a semaphore port name + def initialize(value = 1, opts = {}) + task = opts[:task] || ipc_space || mach_task_self + sync_policy = opts[:sync_policy] || :fifo + + mem = new_memory_pointer(:semaphore_t) + semaphore_create(task, mem, sync_policy, value) + super(mem.get_uint(0), :ipc_space => task) + end + + # Destroy a Semaphore. + # + # @param [Hash] opts + # + # @option opts [Integer] :task the Mach task that owns the + # semaphore (defaults to the owning task) + def destroy(opts = {}) + task = opts[:task] || ipc_space || mach_task_self + semaphore_destroy(task, port) + end + + def signal + semaphore_signal(port) + end + + def signal_all + semaphore_signal_all(port) + end + + def wait + semaphore_wait(port) + end + + # TODO: implement + def timedwait(secs) + semaphore_timedwait(port, timespec) + end + end +end diff --git a/lib/mach/task.rb b/lib/mach/task.rb new file mode 100644 index 0000000..2710d60 --- /dev/null +++ b/lib/mach/task.rb @@ -0,0 +1,40 @@ +require 'mach/functions' +require 'mach/port' + +module Mach + class Task < Port + include Functions + + # @return [Task] + def self.self + new(Functions.mach_task_self) + end + + def initialize(task) + super(task) + end + + alias_method :task, :port + + def get_special_port(which_port) + mem = FFI::MemoryPointer.new(:int) + task_get_special_port(task, which_port, mem) + Port.new(mem.get_int(0)) + end + + # @param [Port,Integer] newport + def set_special_port(which_port, newport) + p = newport.respond_to?(:port) ? newport.port : newport + task_set_special_port(task, which_port, p) + end + + def get_bootstrap_port + get_special_port(:bootstrap) + end + + def set_bootstrap_port(port) + set_special_port(:bootstrap, port) + end + end +end + diff --git a/spec/mach/port_spec.rb b/spec/mach/port_spec.rb new file mode 100644 index 0000000..2b3aa51 --- /dev/null +++ b/spec/mach/port_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' +require 'mach/port' + +module Mach + describe Port do + it 'creates a port' do + port = Port.new + port.destroy + end + + it 'raises exception with invalid args' do + p = proc { Port.new(:right => 1234) } + p.must_raise Error::FAILURE + end + + it 'inserts rights' do + port = Port.new + port.insert_right(:make_send) + end + end +end diff --git a/spec/mach/semaphore_spec.rb b/spec/mach/semaphore_spec.rb new file mode 100644 index 0000000..98e1468 --- /dev/null +++ b/spec/mach/semaphore_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' +require 'mach/error' +require 'mach/functions' +require 'mach/semaphore' + +module Mach + describe 'low level semaphore functions' do + include Functions + + it 'raises exception with invalid args' do + p = proc { semaphore_create(mach_task_self, nil, 1234, 1) } + p.must_raise Error::INVALID_ARGUMENT + end + end + + describe Semaphore do + it 'creates a semaphore' do + sem = Semaphore.new + sem.destroy + end + + it 'raises exception with invalid args' do + p = proc { Semaphore.new(1, :sync_policy => :no_such) } + p.must_raise ArgumentError # Error::INVALID_ARGUMENT + end + + it 'signals/waits in same task' do + sem = Semaphore.new(0) + sem.signal + sem.wait + sem.destroy + end + end +end diff --git a/spec/mach/task_spec.rb b/spec/mach/task_spec.rb new file mode 100644 index 0000000..49f5cbc --- /dev/null +++ b/spec/mach/task_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' +require 'mach' +require 'mach/port' +require 'mach/task' + +module Mach + describe Task do + before :each do + @task = Task.self + end + + it 'gets special ports' do + bp = @task.get_special_port(:bootstrap) + bp.must_equal @task.get_bootstrap_port + end + + it 'redefines bootstrap port' do + bp = @task.get_bootstrap_port + new_bp = Port.new + bp.wont_equal(new_bp) + + begin + new_bp.insert_right(:make_send) + @task.set_bootstrap_port(new_bp) + @task.get_bootstrap_port.must_equal(new_bp) + ensure + @task.set_bootstrap_port(bp) + end + end + end +end