Revision: 1516 http://trac.macosforge.org/projects/ruby/changeset/1516 Author: lsansonetti@apple.com Date: 2009-05-02 13:07:25 -0700 (Sat, 02 May 2009) Log Message: ----------- compile specialized objc stubs for pure ruby methods and insert them into the runtime Modified Paths: -------------- MacRuby/branches/experimental/roxor.cpp MacRuby/branches/experimental/spec/macruby/fixtures/method.m MacRuby/branches/experimental/spec/macruby/method_spec.rb MacRuby/branches/experimental/spec/macruby/objc_method_spec.rb MacRuby/branches/experimental/spec/macruby/spec_helper.rb Added Paths: ----------- MacRuby/branches/experimental/spec/macruby/fixtures/method.rb Modified: MacRuby/branches/experimental/roxor.cpp =================================================================== --- MacRuby/branches/experimental/roxor.cpp 2009-05-02 16:47:43 UTC (rev 1515) +++ MacRuby/branches/experimental/roxor.cpp 2009-05-02 20:07:25 UTC (rev 1516) @@ -68,14 +68,12 @@ unsigned char *start; unsigned char *end; void *imp; - ID mid; RoxorFunction (Function *_f, unsigned char *_start, unsigned char *_end) { f = _f; start = _start; end = _end; imp = NULL; // lazy - mid = 0; // lazy } }; @@ -205,6 +203,7 @@ Function *compile_ffi_function(void *stub, void *imp, int argc); Function *compile_to_rval_convertor(const char *type); Function *compile_to_ocval_convertor(const char *type); + Function *compile_objc_stub(Function *ruby_func, const char *types); const Type *convert_type(const char *type); @@ -451,11 +450,15 @@ struct RoxorFunctionIMP { NODE *node; - SEL sel; + SEL sel; + IMP objc_imp; + IMP ruby_imp; - RoxorFunctionIMP(NODE *_node, SEL _sel) { + RoxorFunctionIMP(NODE *_node, SEL _sel, IMP _objc_imp, IMP _ruby_imp) { node = _node; sel = _sel; + objc_imp = _objc_imp; + ruby_imp = _ruby_imp; } }; @@ -566,6 +569,8 @@ } #endif + std::map<Function *, IMP> objc_to_ruby_stubs; + #if ROXOR_VM_DEBUG long functions_compiled; #endif @@ -582,9 +587,10 @@ void set_running(bool flag) { running = flag; } struct mcache *method_cache_get(SEL sel, bool super); + struct RoxorFunctionIMP *method_func_imp_get(IMP imp); NODE *method_node_get(IMP imp); - void add_method(Class klass, SEL sel, IMP imp, NODE *node, - const char *types); + void add_method(Class klass, SEL sel, IMP imp, IMP ruby_imp, + NODE *node, const char *types); GlobalVariable *redefined_op_gvar(SEL sel, bool create); bool should_invalidate_inline_op(SEL sel, Class klass); @@ -2684,17 +2690,24 @@ return iter->second; } -NODE * -RoxorVM::method_node_get(IMP imp) +inline struct RoxorFunctionIMP * +RoxorVM::method_func_imp_get(IMP imp) { std::map<IMP, struct RoxorFunctionIMP *>::iterator iter = ruby_imps.find(imp); if (iter == ruby_imps.end()) { return NULL; } - return iter->second->node; + return iter->second; } +inline NODE * +RoxorVM::method_node_get(IMP imp) +{ + struct RoxorFunctionIMP *func_imp = method_func_imp_get(imp); + return func_imp == NULL ? NULL : func_imp->node; +} + extern "C" NODE * rb_vm_get_method_node(IMP imp) @@ -2755,7 +2768,7 @@ } void -RoxorVM::add_method(Class klass, SEL sel, IMP imp, NODE *node, +RoxorVM::add_method(Class klass, SEL sel, IMP imp, IMP ruby_imp, NODE *node, const char *types) { #if ROXOR_VM_DEBUG @@ -2772,18 +2785,29 @@ class_replaceMethod(klass, sel, imp, types); // Cache the method node. - NODE *old_node = method_node_get(imp); - if (old_node == NULL && old_node != node) { - ruby_imps[imp] = new RoxorFunctionIMP(node, sel); + std::map<IMP, struct RoxorFunctionIMP *>::iterator iter = + ruby_imps.find(imp); + RoxorFunctionIMP *func_imp; + if (iter == ruby_imps.end()) { + ruby_imps[imp] = func_imp = + new RoxorFunctionIMP(node, sel, imp, ruby_imp); } -// else { -// assert(old_node == node); -// } + else { + func_imp = iter->second; + func_imp->node = node; + func_imp->sel = sel; + func_imp->ruby_imp = ruby_imp; + } +#if 1 + if (imp != ruby_imp) { + ruby_imps[ruby_imp] = func_imp; + } +#endif // Invalidate dispatch cache. - std::map<SEL, struct mcache *>::iterator iter = mcache.find(sel); - if (iter != mcache.end()) { - iter->second->flag = 0; + std::map<SEL, struct mcache *>::iterator iter2 = mcache.find(sel); + if (iter2 != mcache.end()) { + iter2->second->flag = 0; } // Invalidate inline operations. @@ -6210,8 +6234,9 @@ { IMP imp = method_getImplementation(method); const char *types = method_getTypeEncoding(method); - NODE *node = GET_VM()->method_node_get(imp); - if (node == NULL) { + + struct RoxorFunctionIMP *func_imp = GET_VM()->method_func_imp_get(imp); + if (func_imp == NULL) { rb_raise(rb_eArgError, "cannot alias non-Ruby method `%s'", sel_getName(method_getName(method))); } @@ -6227,7 +6252,8 @@ sel = sel_registerName(tmp); } - GET_VM()->add_method(klass, sel, imp, node, types); + GET_VM()->add_method(klass, sel, imp, func_imp->ruby_imp, + func_imp->node, types); } extern "C" @@ -6367,26 +6393,112 @@ return -1; } +Function * +RoxorCompiler::compile_objc_stub(Function *ruby_func, const char *types) +{ + char buf[100]; + const char *p = types; + + p = GetFirstType(p, buf, sizeof buf); + std::string ret_type(buf); + const Type *f_ret_type = convert_type(buf); + + std::vector<const Type *> f_types; + // self + f_types.push_back(RubyObjTy); + p = SkipFirstType(p); + // sel + f_types.push_back(PtrTy); + p = SkipFirstType(p); + std::vector<std::string> arg_types; + for (unsigned int i = 0; i < ruby_func->arg_size() - 2; i++) { + p = GetFirstType(p, buf, sizeof buf); + f_types.push_back(convert_type(buf)); + arg_types.push_back(buf); + } + + FunctionType *ft = FunctionType::get(f_ret_type, f_types, false); + Function *f = cast<Function>(module->getOrInsertFunction("", ft)); + Function::arg_iterator arg = f->arg_begin(); + + bb = BasicBlock::Create("EntryBlock", f); + + std::vector<Value *> params; + params.push_back(arg++); // self + params.push_back(arg++); // sel + + for (unsigned int i = 0; i < ruby_func->arg_size() - 2; i++) { + Value *ruby_arg = compile_conversion_to_ruby(arg_types[i].c_str(), + f_types[i + 2], arg++); + params.push_back(ruby_arg); + } + + Value *ret_val = CallInst::Create(ruby_func, params.begin(), + params.end(), "", bb); + + if (f_ret_type != Type::VoidTy) { + ret_val = compile_conversion_to_c(ret_type.c_str(), ret_val, + new AllocaInst(f_ret_type, "", bb)); + + ReturnInst::Create(ret_val, bb); + } + else { + ReturnInst::Create(bb); + } + + return f; +} + #if ROXOR_ULTRA_LAZY_JIT static void -resolve_method(Class klass, SEL sel, rb_vm_method_source_t *m) +resolve_method(Class klass, SEL sel, Function *func, NODE *node, IMP imp, + Method m) { - IMP imp = GET_VM()->compile(m->func); + const int oc_arity = rb_vm_node_arity(node).real + 3; - const int oc_arity = rb_vm_node_arity(m->node).real; + char types[100]; + bs_element_method_t *bs_method = GET_VM()->find_bs_method(klass, sel); - char *types = (char *)alloca(oc_arity + 4); - types[0] = '@'; - types[1] = '@'; - types[2] = ':'; - for (int i = 0; i < oc_arity; i++) { - types[3 + i] = '@'; + if (m == NULL || !rb_objc_get_types(Qnil, klass, sel, bs_method, types, + sizeof types)) { + assert((unsigned int)oc_arity < sizeof(types)); + types[0] = '@'; + types[1] = '@'; + types[2] = ':'; + for (int i = 3; i < oc_arity; i++) { + types[i] = '@'; + } + types[oc_arity] = '\0'; } - types[3 + oc_arity] = '\0'; + else { + const int m_argc = method_getNumberOfArguments(m); + if (m_argc < oc_arity) { + for (int i = m_argc; i < oc_arity; i++) { + strcat(types, "@"); + } + } + } - GET_VM()->add_method(klass, sel, imp, m->node, types); -} + std::map<Function *, IMP>::iterator iter = + GET_VM()->objc_to_ruby_stubs.find(func); + IMP objc_imp; + if (iter == GET_VM()->objc_to_ruby_stubs.end()) { + Function *objc_func = RoxorCompiler::shared->compile_objc_stub(func, + types); + objc_imp = GET_VM()->compile(objc_func); + GET_VM()->objc_to_ruby_stubs[func] = objc_imp; + } + else { + objc_imp = iter->second; + } + if (imp == NULL) { + imp = GET_VM()->compile(func); + } + + GET_VM()->add_method(klass, sel, objc_imp, imp, node, types); +} + static bool rb_vm_resolve_method(Class klass, SEL sel) { @@ -6427,7 +6539,7 @@ if (k != NULL) { rb_vm_method_source_t *m = iter->second; - resolve_method(iter->first, sel, m); + resolve_method(iter->first, sel, m->func, m->node, NULL, NULL); map->erase(iter++); free(m); did_something = true; @@ -6456,16 +6568,18 @@ const bool genuine_selector = sel_name[strlen(sel_name) - 1] == ':'; bool redefined = false; SEL orig_sel = sel; + Method m; IMP imp = NULL; prepare_method: - if (class_getInstanceMethod(klass, sel) != NULL) { + m = class_getInstanceMethod(klass, sel); + if (m != NULL) { // The method already exists - we need to JIT it. if (imp == NULL) { imp = GET_VM()->compile(func); } - rb_vm_define_method(klass, sel, imp, node, true); + resolve_method(klass, sel, func, node, imp, m); } else { // Let's keep the method and JIT it later on demand. @@ -6798,6 +6912,8 @@ int oc_arity = genuine_selector ? arity.real : 0; bool redefined = direct; + RoxorFunctionIMP *func_imp = GET_VM()->method_func_imp_get(imp); + IMP ruby_imp = func_imp == NULL ? imp : func_imp->ruby_imp; define_method: char *types; @@ -6817,7 +6933,7 @@ types[3 + oc_arity] = '\0'; } - GET_VM()->add_method(klass, sel, imp, node, types); + GET_VM()->add_method(klass, sel, imp, ruby_imp, node, types); if (!redefined) { if (!genuine_selector && arity.max != arity.min) { @@ -7047,7 +7163,7 @@ abort(); } -static inline Method +static /*inline*/ Method rb_vm_super_lookup(VALUE klass, SEL sel, VALUE *klassp) { VALUE k, ary; @@ -7059,13 +7175,29 @@ void *callstack[128]; int callstack_n = backtrace(callstack, 128); + std::vector<void *> callstack_funcs; + for (int i = 0; i < callstack_n; i++) { + void *start = NULL; + if (GET_VM()->symbolize_call_address(callstack[i], + &start, NULL, NULL, 0)) { + struct RoxorFunctionIMP *func_imp = + GET_VM()->method_func_imp_get((IMP)start); + if (func_imp != NULL && func_imp->ruby_imp == start) { + start = (void *)func_imp->objc_imp; + } + callstack_funcs.push_back(start); + } + } + #if ROXOR_VM_DEBUG printf("locating super method %s of class %s in ancestor chain %s\n", sel_getName(sel), rb_class2name(klass), RSTRING_PTR(rb_inspect(ary))); - printf("callstack: "); - for (i = callstack_n - 1; i >= 0; i--) { - printf("%p ", callstack[i]); + printf("callstack functions: "); + for (std::vector<void *>::iterator iter = callstack_funcs.begin(); + iter != callstack_funcs.end(); + ++iter) { + printf("%p ", *iter); } printf("\n"); #endif @@ -7092,21 +7224,12 @@ IMP imp = method_getImplementation(method); - bool on_stack = false; - for (int j = callstack_n - 1; j >= 0; j--) { - void *start = NULL; - if (GET_VM()->symbolize_call_address(callstack[j], - &start, NULL, NULL, 0)) { - if (start == (void *)imp) { - on_stack = true; - break; - } - } - } - - if (!on_stack) { + if (std::find(callstack_funcs.begin(), callstack_funcs.end(), + (void *)imp) == callstack_funcs.end()) { + // Method is not on stack. #if ROXOR_VM_DEBUG - printf("returning method implementation from class/module %s\n", rb_class2name(k)); + printf("returning method implementation %p " \ + "from class/module %s\n", imp, rb_class2name(k)); #endif return method; } @@ -7359,15 +7482,16 @@ if (method != NULL) { IMP imp = method_getImplementation(method); - NODE *node = GET_VM()->method_node_get(imp); + struct RoxorFunctionIMP *func_imp = + GET_VM()->method_func_imp_get(imp); - if (node != NULL) { + if (func_imp != NULL) { // ruby call cache->flag = MCACHE_RCALL; rcache.klass = klass; - rcache.imp = imp; - rcache.node = node; - rcache.arity = rb_vm_node_arity(node); + rcache.imp = func_imp->ruby_imp; + rcache.node = func_imp->node; + rcache.arity = rb_vm_node_arity(func_imp->node); } else { // objc call Modified: MacRuby/branches/experimental/spec/macruby/fixtures/method.m =================================================================== --- MacRuby/branches/experimental/spec/macruby/fixtures/method.m 2009-05-02 16:47:43 UTC (rev 1515) +++ MacRuby/branches/experimental/spec/macruby/fixtures/method.m 2009-05-02 20:07:25 UTC (rev 1516) @@ -32,26 +32,52 @@ { } ++ (BOOL)testMethodReturningVoid:(TestMethod *)o +{ + [o methodReturningVoid]; + return YES; +} + - (id)methodReturningSelf { return self; } ++ (BOOL)testMethodReturningSelf:(TestMethod *)o +{ + return [o methodReturningSelf] == o; +} + - (id)methodReturningNil { return nil; } ++ (BOOL)testMethodReturningNil:(TestMethod *)o +{ + return [o methodReturningNil] == nil; +} + - (id)methodReturningCFTrue { return (id)kCFBooleanTrue; } ++ (BOOL)testMethodReturningCFTrue:(TestMethod *)o +{ + return [o methodReturningCFTrue] == (id)kCFBooleanTrue; +} + - (id)methodReturningCFFalse { return (id)kCFBooleanFalse; } ++ (BOOL)testMethodReturningCFFalse:(TestMethod *)o +{ + return [o methodReturningCFFalse] == (id)kCFBooleanFalse; +} + - (id)methodReturningCFNull { return (id)kCFNull; @@ -62,66 +88,131 @@ return YES; } ++ (BOOL)testMethodReturningYES:(TestMethod *)o +{ + return [o methodReturningYES] == YES; +} + - (BOOL)methodReturningNO { return NO; } ++ (BOOL)testMethodReturningNO:(TestMethod *)o +{ + return [o methodReturningNO] == NO; +} + - (char)methodReturningChar { return (char)42; } ++ (BOOL)testMethodReturningChar:(TestMethod *)o +{ + return [o methodReturningChar] == (char)42; +} + - (char)methodReturningChar2 { return (char)-42; } ++ (BOOL)testMethodReturningChar2:(TestMethod *)o +{ + return [o methodReturningChar2] == (char)-42; +} + - (unsigned char)methodReturningUnsignedChar { return (unsigned char)42; } ++ (BOOL)testMethodReturningUnsignedChar:(TestMethod *)o +{ + return [o methodReturningUnsignedChar] == (unsigned char)42; +} + - (short)methodReturningShort { return (short)42; } ++ (BOOL)testMethodReturningShort:(TestMethod *)o +{ + return [o methodReturningShort] == (short)42; +} + - (short)methodReturningShort2 { return (short)-42; } ++ (BOOL)testMethodReturningShort2:(TestMethod *)o +{ + return [o methodReturningShort2] == (short)-42; +} + - (unsigned short)methodReturningUnsignedShort { return (unsigned short)42; } ++ (BOOL)testMethodReturningUnsignedShort:(TestMethod *)o +{ + return [o methodReturningUnsignedShort] == (unsigned short)42; +} + - (int)methodReturningInt { return 42; } ++ (BOOL)testMethodReturningInt:(TestMethod *)o +{ + return [o methodReturningInt] == (int)42; +} + - (int)methodReturningInt2 { return -42; } ++ (BOOL)testMethodReturningInt2:(TestMethod *)o +{ + return [o methodReturningInt2] == (int)-42; +} + - (unsigned int)methodReturningUnsignedInt { return 42; } ++ (BOOL)testMethodReturningUnsignedInt:(TestMethod *)o +{ + return [o methodReturningUnsignedInt] == (int)42; +} + - (long)methodReturningLong { return 42; } + ++ (BOOL)testMethodReturningLong:(TestMethod *)o +{ + return [o methodReturningLong] == (long)42; +} - (long)methodReturningLong2 { return -42; } ++ (BOOL)testMethodReturningLong2:(TestMethod *)o +{ + return [o methodReturningLong2] == (long)-42; +} + - (long)methodReturningLong3 { #if __LP64__ @@ -145,6 +236,11 @@ return 42; } ++ (BOOL)testMethodReturningUnsignedLong:(TestMethod *)o +{ + return [o methodReturningUnsignedLong] == (unsigned long)42; +} + - (unsigned long)methodReturningUnsignedLong2 { #if __LP64__ Added: MacRuby/branches/experimental/spec/macruby/fixtures/method.rb =================================================================== --- MacRuby/branches/experimental/spec/macruby/fixtures/method.rb (rev 0) +++ MacRuby/branches/experimental/spec/macruby/fixtures/method.rb 2009-05-02 20:07:25 UTC (rev 1516) @@ -0,0 +1,21 @@ +class TestMethodOverride < TestMethod + def methodReturningVoid; 42; end + def methodReturningSelf; self; end + def methodReturningNil; nil; end + def methodReturningCFTrue; true; end + def methodReturningCFFalse; false; end + def methodReturningYES; true; end + def methodReturningNO; false; end + def methodReturningChar; 42; end + def methodReturningChar2; -42; end + def methodReturningUnsignedChar; 42; end + def methodReturningShort; 42; end + def methodReturningShort2; -42; end + def methodReturningUnsignedShort; 42; end + def methodReturningInt; 42; end + def methodReturningInt2; -42; end + def methodReturningUnsignedInt; 42; end + def methodReturningLong; 42; end + def methodReturningLong2; -42; end + def methodReturningUnsignedLong; 42; end +end Modified: MacRuby/branches/experimental/spec/macruby/method_spec.rb =================================================================== --- MacRuby/branches/experimental/spec/macruby/method_spec.rb 2009-05-02 16:47:43 UTC (rev 1515) +++ MacRuby/branches/experimental/spec/macruby/method_spec.rb 2009-05-02 20:07:25 UTC (rev 1516) @@ -55,7 +55,6 @@ @o.send(:'doSomething:withObject:withObject:', 30, 10, 2).should == 42 end -=begin # TODO it "can be called using -[NSObject performSelector:]" do def @o.doSomething; 42; end @o.performSelector(:'doSomething').should == 42 @@ -71,7 +70,6 @@ @o.performSelector(:'doSomething:withObject:', withObject:40, withObject:2).should == 42 end -=end it "cannot be called with #foo=, even if it matches the Objective-C #setFoo pattern" do def @o.setFoo(x); end @@ -82,6 +80,4 @@ def @o.isFoo; end @o.should_not have_method(:'foo?') end - - # TODO add overloading specs end Modified: MacRuby/branches/experimental/spec/macruby/objc_method_spec.rb =================================================================== --- MacRuby/branches/experimental/spec/macruby/objc_method_spec.rb 2009-05-02 16:47:43 UTC (rev 1515) +++ MacRuby/branches/experimental/spec/macruby/objc_method_spec.rb 2009-05-02 20:07:25 UTC (rev 1516) @@ -1,6 +1,8 @@ require File.dirname(__FILE__) + "/spec_helper" FixtureCompiler.require! "method" +require File.dirname(__FILE__) + '/fixtures/method' + describe "A pure Objective-C method" do before :each do @o = TestMethod.new @@ -397,3 +399,80 @@ lambda { @o.methodAcceptingNSRectPtr2(Pointer.new(NSPoint.type)) }.should raise_error(TypeError) end end + +describe "A pure MacRuby method" do + before :each do + @o = TestMethodOverride.new + end + + it "can overwrite an Objective-C method returning void" do + @o.methodReturningVoid.should == 42 + TestMethodOverride.testMethodReturningVoid(@o).should == 1 + end + + it "can overwrite an Objective-C method returning self" do + @o.methodReturningSelf.should == @o + TestMethodOverride.testMethodReturningSelf(@o).should == 1 + end + + it "can overwrite an Objective-C method returning nil as 'id'" do + @o.methodReturningNil.should == nil + TestMethodOverride.testMethodReturningNil(@o).should == 1 + end + + it "can overwrite an Objective-C method returning kCFBooleanTrue as 'id'" do + @o.methodReturningCFTrue.should == true + TestMethodOverride.testMethodReturningCFTrue(@o).should == 1 + end + + it "can overwrite an Objective-C method returning kCFBooleanFalse as 'id'" do + @o.methodReturningCFFalse.should == false + TestMethodOverride.testMethodReturningCFFalse(@o).should == 1 + end + + it "can overwrite an Objective-C method returning YES as 'BOOL'" do + @o.methodReturningYES.should == true + TestMethodOverride.testMethodReturningYES(@o).should == 1 + end + + it "can overwrite an Objective-C method returning NO as 'BOOL'" do + @o.methodReturningNO.should == false + TestMethodOverride.testMethodReturningNO(@o).should == 1 + end + + it "can overwrite an Objective-C method returning 'unsigned char' or 'char'" do + @o.methodReturningChar.should == 42 + @o.methodReturningChar2.should == -42 + @o.methodReturningUnsignedChar.should == 42 + TestMethodOverride.testMethodReturningChar(@o).should == 1 + TestMethodOverride.testMethodReturningChar2(@o).should == 1 + TestMethodOverride.testMethodReturningUnsignedChar(@o).should == 1 + end + + it "can overwrite an Objective-C method returning 'unsigned short' or 'short'" do + @o.methodReturningShort.should == 42 + @o.methodReturningShort2.should == -42 + @o.methodReturningUnsignedShort.should == 42 + TestMethodOverride.testMethodReturningShort(@o).should == 1 + TestMethodOverride.testMethodReturningShort2(@o).should == 1 + TestMethodOverride.testMethodReturningUnsignedShort(@o).should == 1 + end + + it "can overwrite an Objective-C method returning 'unsigned int' or 'int'" do + @o.methodReturningInt.should == 42 + @o.methodReturningInt2.should == -42 + @o.methodReturningUnsignedInt.should == 42 + TestMethodOverride.testMethodReturningInt(@o).should == 1 + TestMethodOverride.testMethodReturningInt2(@o).should == 1 + TestMethodOverride.testMethodReturningUnsignedInt(@o).should == 1 + end + + it "can overwrite an Objective-C method returning 'unsigned long' or 'long'" do + @o.methodReturningLong.should == 42 + @o.methodReturningLong2.should == -42 + @o.methodReturningUnsignedLong.should == 42 + TestMethodOverride.testMethodReturningLong(@o).should == 1 + TestMethodOverride.testMethodReturningLong2(@o).should == 1 + TestMethodOverride.testMethodReturningUnsignedLong(@o).should == 1 + end +end Modified: MacRuby/branches/experimental/spec/macruby/spec_helper.rb =================================================================== --- MacRuby/branches/experimental/spec/macruby/spec_helper.rb 2009-05-02 16:47:43 UTC (rev 1515) +++ MacRuby/branches/experimental/spec/macruby/spec_helper.rb 2009-05-02 20:07:25 UTC (rev 1516) @@ -32,7 +32,7 @@ private def needs_update? - !File.exist?(bundle) or File.mtime(fixture) > File.mtime(fixture) + !File.exist?(bundle) or File.mtime(fixture) > File.mtime(bundle) end def compile! @@ -51,4 +51,4 @@ require bundle[0..-8] load_bridge_support_file bridge_support end -end \ No newline at end of file +end
participants (1)
-
source_changes@macosforge.org