ObjC callback timing

@Simeon @jfperusse

Could either of you tell me how Lua callbacks are called from an ObjC binding?

In this example, I’m executing some javascript but the print in the js bound function aren’t showing up until after the exec returns.

-- JavaScript

function setup()
    
    local js = javascript.context()
    
    js:set("get10", function()
        print("I'm incorrectly printed after the results!")
        return 10
    end)
    
    -- Both these 2 cases should print 10 as the result
    local _, result = js:eval([[
        var x = 10
        x
    ]])
    print("Result 1: ", result)
    
    -- But this case doesn't.
    -- Nor does the print() appear until js:eval returns...
    local _, result = js:eval([[
        var x = get10()
        x
    ]])
    print("Result 2: ", result)
end

Full example in zip file.

Hi @Steppers.

One thing I noticed as I’m working on adding support for delegates is that our Lua callbacks are called asynchronously, which might explain why you are only seeing both results at the end.

I’m currently testing doing the callbacks synchronously as I need this to return a value (e.g. UITextViewDelegate’s textViewShouldBeginEditing).

Once I get that working, I’ll see if your code works as expected. I just hope that the synchronous calls won’t end up causing deadlocks :sweat_smile:

Thanks!

@jfperusse Ah, fantastic! That sounds like exactly the same issue as I’m having but with JavaScriptCore.

As far as I know it should be possible to call back into lua synchronously without issue as it should be reentrant in that regard.

@Steppers what are you reading that tells you JavaScript will be able to call a Lua function?

@RonJeffries The ObjC bindings already allow passing Lua defined callbacks into ObjC functions. I expect it turns them into a native representation of sorts.

This just utilises that to set a global value in the JavaScript context which we can call in JS. It already works, just not when I’d expect it to.

if it’s javascript shouldn’t it be console.log()?

The “set” method actually defines a JavaScript method which results in a call to the specified Lua function, so print is the right thing to use. There is no “conversion into a native representation”. On the native side, we pass the native method an “Objective-C block” which, when called, will simply call the Lua function.

I’m like to learn more about the JavaScript / Codea stuff. Is there material I can read?

@RonJeffries Literally the project I’ve attached in the first post :smile:
As far as I know this is the first time this has been done.

It’s pretty straightforward tbh. Take a look at JavaScriptCore for the ObjC API documentation.

Note that this is still a WIP project and will be released onto WebRepo once it’s in a final state.

@skar The only JavaScript being executed is the string passed to the js:eval calls.

The line var x = get10() in the js actually calls back into the Lua to execute the function I assigned to ‘get10’ above.

Thanks! Very exciting!

I’m wildly guessing here but does the Lua function perhaps need to create and return a Core Foundation number rather than a Lua number? That might explain the nil.

The nil is because the current version runs callbacks asynchronously. So when the second eval is called, the following print statement actually evaluates before the callback could even return a value.

Executing callbacks synchronously did introduce deadlocks so I had to change a few things. I now have the above sample working fine locally but I want to do a bit more testing to make sure I’m not introducing other instabilities :slight_smile:

I look forward to learning how to do it! Thanks!

Hey everyone, just wanted to give a quick update on this.

Implementing callback support and making sure things are evaluated in the right order did open a huge can of worms with deadlocks. I’ve been thinking about how to solve this properly and I believe I have a good solution in mind, but it will still take some time to put in place properly.

I’ll keep you posted as soon as I make progress on it!

Thanks!

@jfperusse Thanks for the update!

Hi everyone, hi @Steppers !

Callbacks are now available in the latest beta, and this includes the fix for the JavaScript timing issue.

Here’s the updated JavaScript sample:

function setup()    
    local js = javascript.context()
    
    js:set("get10", function()
        print("I'm correctly printed between the results!")
        return 10
    end)
    
    local _, result = js:eval([[
        var x = 10
        x
    ]])
    print("Result 1: ", result)
    
    local _, result = js:eval([[
        var x = get10()
        x
    ]])
    print("Result 2: ", result)
end

function draw()
    background(0, 0, 0)
end

And here’s a basic example of the new objc.callback with a text view:

Delegate = objc.delegate("UITextViewDelegate")

function Delegate:init(test)
    self.test = test
end

function Delegate:textViewShouldBeginEditing_(objTextView)
    return AllowBeginEditing
end

function Delegate:textViewDidChange_(objTextView)
    print(self.test .. ": " .. objTextView.text)
end

function endEditing()
    uiTextView:endEditing_(true)
end

function setup()
    parameter.boolean("AllowBeginEditing", false, endEditing)
    parameter.action("EndEditing", endEditing)
    
    local vc = objc.viewer
    
    uiTextView = objc.cls.UITextView()
    vc.view:addSubview_(uiTextView)
    
    uiTextView.translatesAutoresizingMaskIntoConstraints = false
    uiTextView.trailingAnchor:constraintEqualToAnchor_constant_(vc.view.trailingAnchor, -20).active = true
    uiTextView.topAnchor:constraintEqualToAnchor_constant_(vc.view.topAnchor, 20).active = true
    uiTextView.widthAnchor:constraintEqualToConstant_(400).active = true
    uiTextView.heightAnchor:constraintEqualToConstant_(200).active = true
    
    uiTextView.text = ""
    uiTextView.layer.cornerRadius = 8
    
    uiTextView.delegate = Delegate("uiTextView")
    
end

function draw()
    background(40, 40, 50)
end

I fixed as many deadlocks as I could find, but there might still be some left for use cases and timings yet to be encountered. If Codea ever becomes unresponsive for you (a common use case for me was when leaving play), please let me know with any details which might help reproduce the problem.

Thanks!

Can we load a JS library that doesn’t rely too much on JS accessors (like “console”) ? I’m thinking of a math library that can load in SVGs or calculate SVG path data.

@jfperusse Just want to check I’m using this correctly?

This is currently causing Codea to crash entirely…

function MessageHandler(func)
    local Handler = objc.delegate("WKScriptMessageHandler")
    function Handler:userContentController_didReceiveScriptMessage_(objUserContentController, objMessage)
        print("log!")
        --func(objMessage.body)
    end
    return Handler()
end

local logHandler = MessageHandler(function(msg)
    print(msg)
end)

function setup()
    
    local controller = objc.cls.WKUserContentController()
    controller:addScriptMessageHandler_name_(logHandler, "log")
    
    local config = objc.cls.WKWebViewConfiguration()
    config.userContentController = controller
    
    -- New view
    local wk = objc.cls.WKWebView:alloc()
    wk:initWithFrame_configuration_(objc.viewer.view.bounds, config)
        
    -- Load page
    wk:loadHTMLString_baseURL_([[
        Hello World!
        <script>
            window.webkit.messageHandlers.log.postMessage("Hello World!");
        </script>
    ]], objc.cls.NSURL:URLWithString_(""))
    
    -- Display the web view above the GL view
    objc.viewer.view:insertSubview_atIndex_(wk, 1)
    objc.viewer.view.subviews[1]:removeFromSuperview()
end

If I comment out window.webkit.messageHandlers.log.postMessage("Hello World!"); then it at least runs so it’s definitely related to the delegate.

To be clear, I’d expect to see the message ‘log!’ appear in Codea’s log panel.

Cheers!

Hi @Steppers! Seems like I missed this way of setting protocols. The only supported way at the moment is through a “.delegate” member. I will add support for delegate as function arguments as soon as possible.

Thanks!