darknoon / cocoascript-class

Lets you create real ObjC classes in cocoascript so you can implement delegates, subclass builtin types, etc
MIT License
22 stars 8 forks source link

not running correctly on sketch 70.5 #17

Closed zzzJH closed 3 years ago

zzzJH commented 3 years ago

macOS 10.15.7

70.5 image

below 70.5 image

demo code

const objc_super_typeEncoding = '{objc_super="receiver"@"super_class"#}';

// You can store this to call your function. this must be bound to the current instance.
function SuperCall(selector, argTypes, returnType) {
  const func = CFunc("objc_msgSendSuper", [{type: '^' + objc_super_typeEncoding}, {type: ":"}, ...argTypes], returnType);
  return function(...args) {
    const struct = make_objc_super(this, this.superclass());
    const structPtr = MOPointer.alloc().initWithValue_(struct);
    return func(structPtr, selector, ...args);
  };
}

// Recursively create a MOStruct
function makeStruct(def) {
  if (typeof def !== 'object' || Object.keys(def).length == 0) {
    return def;
  }
  const name = Object.keys(def)[0];
  const values = def[name];

  const structure = MOStruct.structureWithName_memberNames_runtime(name, Object.keys(values), Mocha.sharedRuntime());

  Object.keys(values).map( member => {
    structure[member] = makeStruct(values[member]);
  });

  return structure;
}

function make_objc_super(self, cls) {
  return makeStruct({
    objc_super:{
      receiver:self,
      super_class: cls,
    },
  });
}

// Due to particularities of the JS bridge, we can't call into MOBridgeSupport objects directly
// But, we can ask key value coding to do the dirty work for us ;)
function setKeys(o, d) {
  const funcDict = NSMutableDictionary.dictionary()
  funcDict.o = o
  Object.keys(d).map( k => funcDict.setValue_forKeyPath(d[k], "o." + k) )
}

// Use any C function, not just ones with BridgeSupport
function CFunc(name, args, retVal) {
  function makeArgument(a) {
    if (!a) return null;
    const arg = MOBridgeSupportArgument.alloc().init();
    setKeys(arg, {
     type64: a.type,
    });
    return arg;
  }
  const func = MOBridgeSupportFunction.alloc().init();
  setKeys(func, {
    name: name,
    arguments: args.map(makeArgument),
    returnValue: makeArgument(retVal),
  })
  return func;
}

/*
@encode(char*) = "*"
@encode(id) = "@"
@encode(Class) = "#"
@encode(void*) = "^v"
@encode(CGRect) = "{CGRect={CGPoint=dd}{CGSize=dd}}"
@encode(SEL) = ":"
*/

function addStructToBridgeSupport(key, structDef) {
  // OK, so this is probably the nastiest hack in this file.
  // We go modify MOBridgeSupportController behind its back and use kvc to add our own definition
  // There isn't another API for this though. So the only other way would be to make a real bridgesupport file.
  const symbols = MOBridgeSupportController.sharedController().valueForKey('symbols');
  if (!symbols) throw Error("Something has changed within bridge support so we can't add our definitions");
  // If someone already added this definition, don't re-register it.
  if (symbols[key] !== null) return;
  const def = MOBridgeSupportStruct.alloc().init();
  setKeys(def, {
    name: key,
    type: structDef.type,
  });
  symbols[key] = def;
};

// This assumes the ivar is an object type. Return value is pretty useless.
const object_getInstanceVariable = CFunc("object_getInstanceVariable", [{type: "@"}, {type:'*'}, {type: "^@"}], {type: "^{objc_ivar=}"});
// Again, ivar is of object type
const object_setInstanceVariable = CFunc("object_setInstanceVariable", [{type: "@"}, {type:'*'}, {type: "@"}], {type: "^{objc_ivar=}"});

// We need Mocha to understand what an objc_super is so we can use it as a function argument
addStructToBridgeSupport('objc_super', {type:objc_super_typeEncoding});

const SuperInit = SuperCall(NSStringFromSelector("init"), [], {type:"@"});

function ObjCClass(defn) {
  const superclass = defn.superclass || NSObject;
  const className = (defn.className || defn.classname || "ObjCClass1") + NSUUID.UUID().UUIDString()
  const reserved = new Set(['className', 'classname','superclass']);
  var cls = MOClassDescription.allocateDescriptionForClassWithName_superclass_(className, superclass)
  // Add each handler to the class description
  const ivars = [];
  for(var key in defn) {
    const v = defn[key];
    if (typeof v == 'function' && key !== 'init') {
      var selector = NSSelectorFromString(key)
      cls.addInstanceMethodWithSelector_function_(selector, v);
    } else if (!reserved.has(key)) {
       ivars.push(key);
       cls.addInstanceVariableWithName_typeEncoding(key, "@");
    }
  }

  cls.addInstanceMethodWithSelector_function_(NSSelectorFromString('init'), function() {
    const self = SuperInit.call(this);
    ivars.map( name => {
      Object.defineProperty(self, name, {
        get() { return getIvar(self, name) },
        set(v) { object_setInstanceVariable(self, name, v) },
      });
      self[name] = defn[name];
    });
    // If there is a passsed-in init funciton, call it now.
    if (typeof defn.init == 'function') defn.init.call(this);
    return self;
  });

  return cls.registerClass();
};

function getIvar(obj, name) {
  const retPtr = MOPointer.new();
  object_getInstanceVariable(obj, name, retPtr);
  return retPtr.value().retain().autorelease();
}

const MyClass = new ObjCClass({
            // String values create ivar
            _private: 'initial',

            // This is a method on the class.
            test() {
                log('test: ' + this._private);
            },
        });

        const obj = MyClass.new();
log(MyClass.new())
zzzJH commented 3 years ago

fix in sketch 70.6. image