trueinteractions / tint2

Native Javascript Applications
1.59k stars 41 forks source link

Converting Objective C to JS #133

Open JohnLouderback opened 9 years ago

JohnLouderback commented 9 years ago

I'm trying to make a pull request for a feature that can be called to, to determine how long a user has been idle. So far, my attempts to convert this Objective C code to JS has been unsuccessful. I'm hoping to glean some information on how the bridge works. I'm trying to convert:

long OSXUIBinding::GetIdleTime()
    {
        // some of the code for this was from:
        // http://ryanhomer.com/blog/2007/05/31/detecting-when-your-cocoa-application-is-idle/
        CFMutableDictionaryRef properties = 0;
        CFTypeRef obj;
        mach_port_t masterPort;
        io_iterator_t iter;
        io_registry_entry_t curObj;

        IOMasterPort(MACH_PORT_NULL, &masterPort);

        /* Get IOHIDSystem */
        IOServiceGetMatchingServices(masterPort, IOServiceMatching("IOHIDSystem"), &iter);
        if (iter == 0)
        {
            return -1;
        }
        else
        {
            curObj = IOIteratorNext(iter);
        }
        if (IORegistryEntryCreateCFProperties(curObj, &properties, kCFAllocatorDefault, 0) == KERN_SUCCESS && properties != NULL)
        {
            obj = CFDictionaryGetValue(properties, CFSTR("HIDIdleTime"));
            CFRetain(obj);
        }
        else
        {
            return -1;
        }

        uint64_t tHandle = 0;
        if (obj)
        {
            CFTypeID type = CFGetTypeID(obj);

            if (type == CFDataGetTypeID())
            {
                CFDataGetBytes((CFDataRef) obj, CFRangeMake(0, sizeof(tHandle)), (UInt8*) &tHandle);
            }
            else if (type == CFNumberGetTypeID())
            {
                CFNumberGetValue((CFNumberRef)obj, kCFNumberSInt64Type, &tHandle);
            }
            else
            {
                // error
                tHandle = 0;
            }

            CFRelease(obj);

            tHandle /= 1000000; // return as milliseconds
        }
        else
        {
            tHandle = -1;
        }

        CFRelease((CFTypeRef)properties);
        IOObjectRelease(curObj);
        IOObjectRelease(iter);
        return (long)tHandle;
    }

With the following code:

require('Common');
var $ = process.bridge.objc;

var getIdleTime = function() {
    debugger;
    var properties = $.alloc($.NSMutableDictionary).ref(),
        obj,
        masterPort = $.alloc().ref(),
        iter = $.alloc().ref(),
        curObj,
        KERN_SUCCESS = 0;

    $.IOMasterPort($.MACH_PORT_NULL, masterPort);

    $.IOServiceGetMatchingServices(masterPort, $.IOServiceMatching("IOHIDSystem"), iter);

    if (iter == 0) {
        return -1;
    }
    else {
        curObj = $.IOIteratorNext(iter);
    }
    if ($.IORegistryEntryCreateCFProperties(curObj, properties, $.kCFAllocatorDefault, 0) == $.KERN_SUCCESS && properties != null) {
        obj = $.CFDictionaryGetValue(properties, $("HIDIdleTime"));
        $.CFRetain(obj);
    }
    else {
        return -1;
    }

    var tHandle = $.alloc(0).ref();

    if (obj) {
        var type = $.CFGetTypeID(obj);

        if (type == $.CFDataGetTypeID())
        {
            $.CFDataGetBytes(obj, $.CFRangeMake(0, sizeof(tHandle)), tHandle);
        }
        else if (type == $.CFNumberGetTypeID())
        {
            $.CFNumberGetValue(obj, $.kCFNumberSInt64Type, tHandle);
        }
        else
        {
            // error
            tHandle = 0;
        }

        $.CFRelease(obj);

        tHandle /= 1000000; // return as milliseconds
    } else {
        tHandle = -1;
    }

    $.CFRelease(properties);
    $.IOObjectRelease(curObj);
    $.IOObjectRelease(iter);
    return tHandle;
}   

setInterval(function() {
    console.log(getIdleTime());
}, 1000);

It always returns -1 at if ($.IORegistryEntryCreateCFProperties(curObj, properties, $.kCFAllocatorDefault, 0) == $.KERN_SUCCESS && properties != null), however.

trevorlinton commented 9 years ago

@JohnLouderback

I can take a closer look at this tomorrow, the one weakness of Tint (and all objective-c bridges) is they sometimes cannot pick up constants that are defined as macros in C++ (these are converted to literal numbers during a pre-compile phase).

So, basically, anything in all capital letters won't be defined for example; $.KERN_SUCCESS will be undefined (although you set KERN_SUCCESS = 0 earlier), also $. MACH_PORT_NULL will be undefined. You can find out the values by creating a simple C file to print out the value of it (usually as a hexadecimal) or find the .h header in the SDK to see what values it has.

I'll try my luck tomorrow and let you know, but at the first pass all of the functions you're requesting are defined in Tint, you can use a repl to inspect what came through the bridge on process.bridge.objc or $ as you have in this.

JohnLouderback commented 9 years ago

Hi Trevor,

Thanks for the information on the constants. I did notice that with KERN_SUCCESS, which is why I set it up top. It looks like I forgot to remove the $, however. I also made a change to initialize MACH_PORT_NULL at the top too. I think my problem is with passing the variables by reference or value... or at least that's one of my problems. I'm not entirely sure how to create a reference to a variable with no value. Also, I'm concerned about the casting between Objective C and JS. For instance: a variable of type CFMutableDictionaryRef is initialized with a value of 0 in Objective C. Knowing that 0 is going to be an Number in JS, I'm concerned that it won't be coerced correctly in Objective C. All that, and then I need reference to this variable. I'm getting relatively proficient in converting C# code to JS, but there are some idiosyncrasies of Objective C that are eluding me when it comes to writing them in JS. I really appreciate your help and insight. Once I understand better how to translate certain concepts, I should be able to make some worth-while contributions.

By the way, here is my updated code:

require('Common');
var $ = process.bridge.objc;

var getIdleTime = function() {
    debugger;
    var properties = $.alloc(0).ref(),
        obj,
        masterPort = $.alloc().ref(),
        iter = $.alloc().ref(),
        curObj,
        KERN_SUCCESS = 0,
        MACH_PORT_NULL = 0;

    $.IOMasterPort(MACH_PORT_NULL, masterPort);

    $.IOServiceGetMatchingServices(masterPort, $.IOServiceMatching("IOHIDSystem"), iter);

    if (iter == 0) {
        return -1;
    }
    else {
        curObj = $.IOIteratorNext(iter);
    }
    if ($.IORegistryEntryCreateCFProperties(curObj, properties, $.kCFAllocatorDefault, 0) == KERN_SUCCESS && properties != null) {
        obj = $.CFDictionaryGetValue(properties, $("HIDIdleTime"));
        $.CFRetain(obj);
    }
    else {
        return -1;
    }

    var tHandle = $.alloc(0).ref();

    if (obj) {
        var type = $.CFGetTypeID(obj);

        if (type == $.CFDataGetTypeID())
        {
            $.CFDataGetBytes(obj, $.CFRangeMake(0, sizeof(tHandle)), tHandle);
        }
        else if (type == $.CFNumberGetTypeID())
        {
            $.CFNumberGetValue(obj, $.kCFNumberSInt64Type, tHandle);
        }
        else
        {
            // error
            tHandle = 0;
        }

        $.CFRelease(obj);

        tHandle /= 1000000; // return as milliseconds
    } else {
        tHandle = -1;
    }

    $.CFRelease(properties);
    $.IOObjectRelease(curObj);
    $.IOObjectRelease(iter);
    return tHandle;
}   

setInterval(function() {
    console.log(getIdleTime());
}, 1000);
JohnLouderback commented 9 years ago

I don't know if it's helpful, but it seems the reason this may not be working is because I'm passing by reference in certain places I should pass by value. It seems that the deref() method crashes Tint though.

trevorlinton commented 9 years ago

@JohnLouderback You should check the address you're using on the deref(), (just use .address()) if the address is null you'll chase down a null pointer and Tint will crash. Just like in C, dereference a pointer that leads to no where (or to protected memory, when you're only supposed to pass it) and the world will collide.

I'm still playing around with your code sample, I think a good debug exercise might be to print out the address of all the pointers in actual C, then try it in tint and see what happens.

BTW, in C, you can use fprintf(stderr, "%p\n", somepointer); in Tint you'd do console.log(somepointer.address())

Also, dereferencing in C uses *pointer, in tint it would be somepointer.deref(), in C a reference is &pointer, in tint its somepointer.ref(). However passing references in C and tint are the same, somefunc(somepointer);

Hope this helps.

JohnLouderback commented 9 years ago

Thanks Trevor, that gives me a lot of insight into how this works. I've spent the better part of today trying to get this to work and learned quite a bit along the way, but no matter what I've done $.IORegistryEntryCreateCFProperties(curObj, properties, $.kCFAllocatorDefault, 0) never returns the success code of zero. It appears the error suggests that the port number is invalid. I didn't have any success converting masterPort from a buffer to an integer so I could compare it with the masterPort value in Objective C. I tried all of the readUInt methods on the buffer to see if I could get back the correct port number, but unfortunately none of the numbers were the expected value, or even close to it, for that matter. I had great success using CGEventSourceSecondsSinceLastEventType as a one-liner, but I don't want to use a deprecated API. I've also tried searching this repo for instances where you switched between buffers and their primitive values frequently, but I couldn't find any. If you have any more insight or advice, I'd love to hear it.

trevorlinton commented 9 years ago

@JohnLouderback

I've looked into this and with a few minor tweeks was able to get to the point where calls into ioiteratornext is valid.. however I ran into the same problems you've had referenced above, an invalid iterator object.

On closer inspection it appears were using mach ports through libuv and through the ffi bridge. Which the closest i can get to an answer is it "locks the mach port", e.g., making a mach port call locks the mach port making it sort of impossible to do mach port calls without compiling it down to native code and loading it through a standard node require();

I really don't have a whole lot to add here, but heres the code I validated was correct (at least to line 41).

require('Common');
var $ = process.bridge.objc;
var $$ = process.bridge;

$.import('CoreFoundation');
$.import('IOKit');

var GetIdleTime = function() {
    var properties = $.alloc($$.ref.types.void),
        obj,
        masterPort = $.alloc($$.ref.types.uint32),
        iter = $.alloc($$.ref.types.uint32),
        curObj,
        KERN_SUCCESS = 0,
        MACH_PORT_NULL = 0;

        console.log($.kIOMasterPortDefault);
    var result = $.IOMasterPort(MACH_PORT_NULL, masterPort);

    var defreffed = masterPort.deref();
    console.log('result: ' + result + ' MACH_PORT_NULL: ',MACH_PORT_NULL,' masterPort.ref: ' , masterPort , ' masterPort: ',masterPort.deref());

    var ioSM = $.IOServiceMatching("IOHIDSystem");
    console.log('ioSM is: ', ioSM);
    result = $.IOServiceGetMatchingServices(masterPort.deref(), ioSM);
    console.log('result: ', result);
    result = $.IOIteratorIsValid(iter);
    console.log('is iterator valid result ' , result);
    if (iter.isNull() || result === 0) {
      console.log('iterator was null.');
      return -1;
    }
    else {
      curObj = $.IOIteratorNext(iter.deref());
    }
    console.log('curObj is: ',curObj);
    var ioreturn = $.IORegistryEntryCreateCFProperties(curObj, properties.ref(), $.kCFAllocatorDefault, 0);
    if (ioreturn == KERN_SUCCESS && properties.address() != 0) {
        obj = $.CFDictionaryGetValue(properties, $$.ref.allocCString("HIDIdleTime"));
        $.CFRetain(obj);
    }
    else {
        return -1;
    }

    var tHandle = $.alloc(0).ref();

    if (obj) {
        var type = $.CFGetTypeID(obj);

        if (type == $.CFDataGetTypeID())
        {
            $.CFDataGetBytes(obj, $.CFRangeMake(0, sizeof(tHandle)), tHandle);
        }
        else if (type == $.CFNumberGetTypeID())
        {
            $.CFNumberGetValue(obj, $.kCFNumberSInt64Type, tHandle);
        }
        else
        {
            // error
            tHandle = 0;
        }

        $.CFRelease(obj);

        tHandle /= 1000000; // return as milliseconds
    } else {
        tHandle = -1;
    }

    $.CFRelease(properties);
    $.IOObjectRelease(curObj);
    $.IOObjectRelease(iter);
    return tHandle;
}   
console.log(GetIdleTime());
trevorlinton commented 9 years ago

@JohnLouderback As a side note, i don't see any notice that Quartz' CGEventSourceSecondsSinceLastEventType is deprecated, theres no notice on apple's docs:

https://developer.apple.com/library/mac/documentation/Carbon/Reference/QuartzEventServicesRef/#//apple_ref/c/func/CGEventSourceSecondsSinceLastEventType

Perhaps you can use that instead? There may also be an objective-c class laying around somewhere that returns the same values.