1/*
2 * Copyright (c) 2006-2008, The RubyCocoa Project.
3 * Copyright (c) 2001-2006, FUJIMOTO Hisakuni.
4 * All Rights Reserved.
5 *
6 * RubyCocoa is free software, covered under either the Ruby's license or the
7 * LGPL. See the COPYRIGHT file for more information.
8 */
9
10#import <Foundation/Foundation.h>
11#import <ctype.h>
12#import "RBObject.h"
13#import "mdl_osxobjc.h"
14#import "ocdata_conv.h"
15#import "BridgeSupport.h"
16#import "internal_macros.h"
17#import "OverrideMixin.h"
18
19#define RBOBJ_LOG(fmt, args...) DLOG("RBOBJ", fmt, ##args)
20
21extern ID _relaxed_syntax_ID;
22
23#if MAC_OS_X_VERSION_MAX_ALLOWED <= MAC_OS_X_VERSION_10_4
24// On MacOS X 10.4 or earlier, +signatureWithObjCTypes: is a SPI
25@interface NSMethodSignature (WarningKiller)
26+ (id) signatureWithObjCTypes:(const char*)types;
27@end
28#endif
29
30static RB_ID sel_to_mid(VALUE rcv, SEL a_sel)
31{
32  int i, length;
33  const char *selName;
34  char mname[1024];
35
36  selName = sel_getName(a_sel);
37  memset(mname, 0, sizeof(mname));
38  strncpy(mname, selName, sizeof(mname) - 1);
39
40  // selstr.sub(/:/,'_')
41  length = strlen(selName);
42  for (i = 0; i < length; i++)
43    if (mname[i] == ':') mname[i] = '_';
44
45  if (RTEST(rb_ivar_get(osx_s_module(), _relaxed_syntax_ID)) && !rb_respond_to(rcv, rb_intern(mname))) {
46    // sub(/^(.*)_$/, "\1")
47    for (i = length - 1; i >= 0; i--) {
48      if (mname[i] != '_') break;
49      mname[i] = '\0';
50    }
51  }
52
53  return rb_intern(mname);
54}
55
56static RB_ID sel_to_mid_as_setter(SEL a_sel)
57{
58  volatile VALUE str = rb_str_new2(sel_getName(a_sel));
59
60  // if str.sub!(/^set([A-Z][^:]*):$/, '\1=') then
61  //   str = str[0].chr.downcase + str[1..-1]
62  // end
63  const char* re_pattern = "^set([A-Z][^:]*):$";
64  VALUE re = rb_reg_new(re_pattern, strlen(re_pattern), 0);
65  if (rb_funcall(str, rb_intern("sub!"), 2, re, rb_str_new2("\\1=")) != Qnil) {
66    int c = (int)(RSTRING(str)->ptr[0]);
67    c = tolower(c);
68    RSTRING(str)->ptr[0] = (char) c;
69  }
70
71  return rb_to_id(str);
72}
73
74static RB_ID rb_obj_sel_to_mid(VALUE rcv, SEL a_sel)
75{
76  RB_ID mid = sel_to_mid(rcv, a_sel);
77  if (rb_respond_to(rcv, mid) == 0)
78    mid = sel_to_mid_as_setter(a_sel);
79  return mid;
80}
81
82static int rb_obj_arity_of_method(VALUE rcv, SEL a_sel, BOOL *ok)
83{
84  VALUE mstr;
85  RB_ID mid;
86  VALUE method;
87  VALUE argc;
88
89  mid = rb_obj_sel_to_mid(rcv, a_sel);
90  if (rb_respond_to(rcv, mid) == 0) {
91    *ok = NO;
92    return 0;
93  }
94  mstr = rb_str_new2(rb_id2name(mid)); // mstr = sel_to_rbobj (a_sel);
95  method = rb_funcall(rcv, rb_intern("method"), 1, mstr);
96  *ok = YES;
97  argc = rb_funcall(method, rb_intern("arity"), 0);
98  return NUM2INT(argc);
99}
100
101@interface __RBObjectThreadDispatcher : NSObject
102{
103  id _returned_ocid;
104  RBObject * _rbobj;
105  NSInvocation * _invocation;
106}
107+ (void)dispatchInvocation:(NSInvocation *)invocation toRBObject:(RBObject *)rbobj;
108@end
109
110@implementation RBObject
111
112// private methods
113
114- (BOOL)rbobjRespondsToSelector: (SEL)a_sel
115{
116  BOOL ret;
117  RB_ID mid;
118  int state;
119  extern void Init_stack(VALUE*);
120
121  if (FREQUENTLY_INIT_STACK_FLAG) {
122    RBOBJ_LOG("rbobjRespondsToSelector(%s) w/Init_stack(%08lx)", a_sel, (void*)&state);
123    Init_stack((void*)&state);
124  }
125  mid = rb_obj_sel_to_mid(m_rbobj, a_sel);
126  ret = (rb_respond_to(m_rbobj, mid) != 0);
127  RBOBJ_LOG("   --> %d", ret);
128  return ret;
129}
130
131- (VALUE)fetchForwardArgumentsOf: (NSInvocation*)an_inv
132{
133  int i;
134  NSMethodSignature* msig = [an_inv methodSignature];
135  int arg_cnt = ([msig numberOfArguments] - 2);
136  VALUE args = rb_ary_new2(arg_cnt);
137  for (i = 0; i < arg_cnt; i++) {
138    VALUE arg_val;
139    const char* octstr = [msig getArgumentTypeAtIndex: (i+2)];
140    void* ocdata = OCDATA_ALLOCA(octstr);
141    BOOL f_conv_success;
142
143    RBOBJ_LOG("arg[%d] of type '%s'", i, octstr);
144    [an_inv getArgument: ocdata atIndex: (i+2)];
145    f_conv_success = ocdata_to_rbobj(Qnil, octstr, ocdata, &arg_val, NO);
146    if (f_conv_success == NO) {
147      arg_val = Qnil;
148    }
149    rb_ary_store(args, i, arg_val);
150  }
151  return args;
152}
153
154- (BOOL)stuffForwardResult: (VALUE)result to: (NSInvocation*)an_inv returnedOcid: (id *) returnedOcid
155{
156  NSMethodSignature* msig = [an_inv methodSignature];
157  const char* octype_str = encoding_skip_to_first_type([msig methodReturnType]);
158  BOOL f_success;
159
160  RBOBJ_LOG("stuff forward result of type '%s'", octype_str);
161
162  *returnedOcid = nil;
163
164  if (*octype_str == _C_VOID) {
165    f_success = true;
166  }
167  else if ((*octype_str == _C_ID) || (*octype_str == _C_CLASS)) {
168    id ocdata = rbobj_get_ocid(result);
169    if (ocdata == nil) {
170      if (result == m_rbobj) {
171        ocdata = self;
172      }
173      else {
174        rbobj_to_nsobj(result, &ocdata);
175        *returnedOcid = ocdata;
176      }
177    }
178    [an_inv setReturnValue: &ocdata];
179    f_success = YES;
180  }
181  else {
182    void* ocdata = OCDATA_ALLOCA(octype_str);
183    f_success = rbobj_to_ocdata (result, octype_str, ocdata, YES);
184    if (f_success) [an_inv setReturnValue: ocdata];
185  }
186  return f_success;
187}
188
189static void
190rbobjRaiseRubyException (void)
191{
192  VALUE lasterr = rb_gv_get("$!");
193  RB_ID mtd = rb_intern("nsexception");
194  if (rb_respond_to(lasterr, mtd)) {
195      VALUE nso = rb_funcall(lasterr, mtd, 0);
196      NSException *exc = rbobj_get_ocid(nso);
197      [exc raise];
198      return; // not reached
199  }
200
201  NSMutableDictionary *info = [NSMutableDictionary dictionary];
202
203  id ocdata = rbobj_get_ocid(lasterr);
204  if (ocdata == nil) {
205      rbobj_to_nsobj(lasterr, &ocdata);
206  }
207  [info setObject: ocdata forKey: @"$!"];
208
209  VALUE klass = rb_class_path(CLASS_OF(lasterr));
210  NSString *rbclass = [NSString stringWithUTF8String:StringValuePtr(klass)];
211
212  VALUE rbmessage = rb_obj_as_string(lasterr);
213  NSString *message = [NSString stringWithUTF8String:StringValuePtr(rbmessage)];
214
215  NSMutableArray *backtraceArray = [NSMutableArray array];
216  volatile VALUE ary = rb_funcall(ruby_errinfo, rb_intern("backtrace"), 0);
217  int c;
218  for (c=0; c<RARRAY(ary)->len; c++) {
219      const char *path = StringValuePtr(RARRAY(ary)->ptr[c]);
220      NSString *nspath = [NSString stringWithUTF8String:path];
221      [backtraceArray addObject: nspath];
222  }
223
224  [info setObject: backtraceArray forKey: @"backtrace"];
225
226  NSException* myException = [NSException
227      exceptionWithName:[@"RBException_" stringByAppendingString: rbclass]
228                         reason:message
229                         userInfo:info];
230  [myException raise];
231}
232
233static VALUE rbobject_protected_apply(VALUE a)
234{
235  VALUE *args = (VALUE*) a;
236  return rb_apply(args[0],(RB_ID)args[1],(VALUE)args[2]);
237}
238
239static void notify_error(VALUE rcv, RB_ID mid)
240{
241  extern int RBNotifyException(const char* title, VALUE err);
242  char title[128];
243  snprintf(title, sizeof(title), "%s#%s", rb_obj_classname(rcv), rb_id2name(mid));
244  RBNotifyException(title, ruby_errinfo);
245}
246
247VALUE rbobj_call_ruby(id rbobj, SEL selector, VALUE args)
248{
249  VALUE m_rbobj;
250  RB_ID mid;
251  VALUE stub_args[3];
252  VALUE rb_result;
253  int err;
254
255  if ([rbobj respondsToSelector:@selector(__rbobj__)]) {
256    m_rbobj = [rbobj __rbobj__];
257  }
258  else if ([rbobj respondsToSelector:@selector(__rbclass__)]) {
259    m_rbobj = [rbobj __rbclass__];
260  }
261  else {
262    // Not an RBObject class, try to get the value from the cache.
263    m_rbobj = ocid_to_rbobj_cache_only(rbobj);
264    if (NIL_P(m_rbobj)) {
265      // Nothing in the cache, it means that this Objective-C object never
266      // crossed the bridge yet. Let's create the Ruby proxy.
267      m_rbobj = ocid_to_rbobj(Qnil, rbobj);
268    }
269  }
270
271  mid = rb_obj_sel_to_mid(m_rbobj, selector);
272  stub_args[0] = m_rbobj;
273  stub_args[1] = mid;
274  stub_args[2] = args;
275
276  RBOBJ_LOG("calling method %s on Ruby object %p with %d args", rb_id2name(mid), m_rbobj, RARRAY(args)->len);
277
278  if (rb_respond_to(m_rbobj, mid) == 0) {
279    VALUE str = rb_inspect(m_rbobj);
280    rb_raise(rb_eRuntimeError, "Ruby object `%s' doesn't respond to the ObjC selector `%s', the method either doesn't exist or is private", StringValuePtr(str), (char *)selector);
281  }
282
283  rb_result = rb_protect(rbobject_protected_apply, (VALUE)stub_args, &err);
284  if (err) {
285    notify_error(m_rbobj, mid);
286    RBOBJ_LOG("got Ruby exception, raising Objective-C exception");
287    rbobjRaiseRubyException();
288    return Qnil; /* to be sure */
289  }
290
291  return rb_result;
292}
293
294- (id)rbobjForwardInvocation: (NSInvocation *)an_inv
295{
296  VALUE rb_args;
297  VALUE rb_result;
298  VALUE rb_result_inspect;
299  id returned_ocid;
300
301  RBOBJ_LOG("rbobjForwardInvocation(%@)", an_inv);
302  rb_args = [self fetchForwardArgumentsOf: an_inv];
303  rb_result = rbobj_call_ruby(self, [an_inv selector], rb_args);
304  [self stuffForwardResult: rb_result to: an_inv returnedOcid: &returned_ocid];
305  rb_result_inspect = rb_inspect(rb_result);
306  RBOBJ_LOG("   --> rb_result=%s", StringValuePtr(rb_result_inspect));
307
308  return returned_ocid;
309}
310
311// public class methods
312+ RBObjectWithRubyScriptCString: (const char*) cstr
313{
314  return [[[self alloc] initWithRubyScriptCString: cstr] autorelease];
315}
316
317+ RBObjectWithRubyScriptString: (NSString*) str
318{
319  return [[[self alloc] initWithRubyScriptString: str] autorelease];
320}
321
322// public methods
323
324- (VALUE) __rbobj__  { return m_rbobj; }
325
326- (void) trackRetainReleaseOfRubyObject
327{
328  VALUE str = rb_inspect(m_rbobj);
329  RBOBJ_LOG("start tracking retain/release of Ruby object `%s'",
330    StringValuePtr(str));
331  m_rbobj_retain_release_track = YES;
332}
333
334- (void) retainRubyObject
335{
336  if (m_rbobj_retain_release_track && !m_rbobj_retained) {
337    VALUE str = rb_inspect(m_rbobj);
338    RBOBJ_LOG("retaining Ruby object `%s'", StringValuePtr(str));
339    rb_gc_register_address(&m_rbobj);
340    m_rbobj_retained = YES;
341  }
342}
343
344- (void) releaseRubyObject
345{
346  if (m_rbobj_retain_release_track && m_rbobj_retained) {
347    RBOBJ_LOG("releasing Ruby object `#<%s:%p>'", rb_obj_classname(m_rbobj), m_rbobj);
348    rb_gc_unregister_address(&m_rbobj);
349    m_rbobj_retained = NO;
350  }
351}
352
353- (void) dealloc
354{
355  RBOBJ_LOG("deallocating RBObject %p", self);
356  remove_from_rb2oc_cache(m_rbobj);
357  [self releaseRubyObject];
358  [super dealloc];
359}
360
361- _initWithRubyObject: (VALUE)rbobj retains: (BOOL) flag
362{
363  m_rbobj = rbobj;
364  m_rbobj_retained = flag;
365  m_rbobj_retain_release_track = NO;
366  oc_master = nil;
367  if (flag)
368    rb_gc_register_address(&m_rbobj);
369  return self;
370}
371
372- initWithRubyObject: (VALUE)rbobj
373{
374  return [self _initWithRubyObject: rbobj retains: YES];
375}
376
377- initWithRubyScriptCString: (const char*) cstr
378{
379  return [self initWithRubyObject: rb_eval_string(cstr)];
380}
381
382- initWithRubyScriptString: (NSString*) str
383{
384  return [self initWithRubyScriptCString: [str UTF8String]];
385}
386
387- (NSString*) _copyDescription
388{
389  VALUE str = rb_inspect(m_rbobj);
390  return [[[NSString alloc] initWithUTF8String: StringValuePtr(str)] autorelease];
391}
392
393- (BOOL)isKindOfClass: (Class)klass
394{
395  BOOL ret;
396  RBOBJ_LOG("isKindOfClass(%@)", NSStringFromClass(klass));
397  ret = NO;
398  RBOBJ_LOG("   --> %d", ret);
399  return ret;
400}
401
402- (BOOL)isRBObject
403{
404  return YES;
405}
406
407- (void)forwardInvocation: (NSInvocation *)an_inv
408{
409  RBOBJ_LOG("forwardInvocation(%@)", an_inv);
410  if ([self rbobjRespondsToSelector: [an_inv selector]]) {
411    RBOBJ_LOG("   -> forward to Ruby Object");
412    if (is_ruby_native_thread()) {
413      [self rbobjForwardInvocation: an_inv];
414    }
415    else {
416      rb_warning("Invocation `%s' received from another thread - forwarding it to the main thread", [[an_inv description] UTF8String]);
417      [__RBObjectThreadDispatcher dispatchInvocation:an_inv toRBObject:self];
418    }
419  }
420  else {
421    RBOBJ_LOG("   -> forward to super Objective-C Object");
422    [super forwardInvocation: an_inv];
423  }
424}
425
426- (NSMethodSignature*)methodSignatureForSelector: (SEL)a_sel
427{
428  NSMethodSignature* ret = nil;
429  RBOBJ_LOG("methodSignatureForSelector(%s)", a_sel);
430  if (a_sel == NULL)
431    return nil;
432  // Try the master object.
433  if (oc_master != nil) {
434    ret = [oc_master instanceMethodSignatureForSelector:a_sel];
435    if (ret != nil)
436      RBOBJ_LOG("\tgot method signature from the master object");
437  }
438  // Try the metadata.
439  if (ret == nil) {
440    struct bsInformalProtocolMethod *method;
441
442    method = find_bs_informal_protocol_method((const char *)a_sel, NO);
443    if (method != NULL) {
444      ret = [NSMethodSignature signatureWithObjCTypes:method->encoding];
445      RBOBJ_LOG("\tgot method signature from metadata (types: '%s')", method->encoding);
446    }
447  }
448  // Ensure a dummy method signature ('id' for everything).
449  if (ret == nil) {
450    int argc;
451    BOOL ok;
452
453    argc = rb_obj_arity_of_method(m_rbobj, a_sel, &ok);
454    if (ok) {
455      char encoding[128], *p;
456
457      if (argc < 0)
458        argc = -1 - argc;
459      argc = MIN(sizeof(encoding) - 4, argc);
460
461      strcpy(encoding, "@@:");
462      p = &encoding[3];
463      while (argc-- > 0) {
464        *p++ = '@';
465      }
466      *p = '\0';
467      ret = [NSMethodSignature signatureWithObjCTypes:encoding];
468      RBOBJ_LOG("\tgenerated dummy method signature");
469    }
470    else {
471      VALUE str = rb_inspect(m_rbobj);
472      RBOBJ_LOG("\tcan't generate a dummy method signature because receiver %s doesn't respond to the selector", StringValuePtr(str));
473    }
474  }
475  RBOBJ_LOG("   --> %@", ret);
476  return ret;
477}
478
479- (BOOL)respondsToSelector: (SEL)a_sel
480{
481  BOOL ret;
482  if (a_sel == @selector(__rbobj__))
483    return YES;
484  RBOBJ_LOG("respondsToSelector(%s)", a_sel);
485  ret = [self rbobjRespondsToSelector: a_sel];
486  RBOBJ_LOG("   --> %d", ret);
487  return ret;
488}
489
490@end
491
492@implementation __RBObjectThreadDispatcher
493
494- (id)initWithInvocation:(NSInvocation *)invocation RBObject:(RBObject *)rbobj
495{
496  self = [super init];
497  if (self != NULL) {
498    _returned_ocid = nil;
499    _invocation = invocation; // no retain
500    _rbobj = rbobj; // no retain
501  }
502  return self;
503}
504
505extern NSThread *rubycocoaThread;
506
507- (void)dispatch
508{
509  DISPATCH_ON_RUBYCOCOA_THREAD(self, @selector(syncDispatch));
510  if (_returned_ocid != nil)
511    [_returned_ocid autorelease];
512}
513
514- (void)syncDispatch
515{
516  _returned_ocid = [_rbobj rbobjForwardInvocation:_invocation];
517  if (_returned_ocid != nil)
518    [_returned_ocid retain];
519}
520
521+ (void)dispatchInvocation:(NSInvocation *)invocation toRBObject:(RBObject *)rbobj
522{
523  __RBObjectThreadDispatcher *  dispatcher;
524
525  dispatcher = [[__RBObjectThreadDispatcher alloc] initWithInvocation:invocation RBObject:rbobj];
526  [dispatcher dispatch];
527  [dispatcher release];
528}
529
530@end
531
532@implementation NSProxy (RubyCocoaEx)
533
534- (BOOL)isRBObject
535{
536  return NO;
537}
538
539@end
540