Lightroom SDK: Many methods yield, preventing robust plugins

  • 3
  • Problem
  • Updated 1 year ago
  • (Edited)
A number of SDK methods do a yield, allowing other tasks to run while the method is being called.  This makes it extremely difficult (practically impossible) to implement robust plugins.

The basic advantage of cooperative, non-preemptive tasks is that the programmer doesn't need to worry about locking.  It is trivial (or should be trivial) to write blocks of code that are guaranteed run atomically.  But that guarantee goes out the window when many SDK methods can yield to other tasks.

As an example, consider a floating dialog that wishes to obtain the crop information of the current photo in the Develop module.  It could do these calls:
photo:getRawMetadata ("width")
photo:getRawMetadata ("height")
LrDevelopController.getValue ("CropLeft")
LrDevelopController.getValue ("CropRight")
LrDevelopController.getValue ("CropTop")
LrDevelopController.getValue ("CropBottom")
But getRawMetadata() can yield, allowing the user to switch to Library, and the calls to getValue() would then return nil, creating a nasty bug.  (* See footnote)

Yes, it's theoretically possible to check for every situation like this and think of clever ways to handle each one.  But putting aside that there's no documentation on which methods yield, it's very likely that even the best programmer would find it difficult to defend correctly against unexpected yields.

You might think of using LrFunctionContext.callWithContext_noyield()to execute its block of code atomically.  But at least with some methods such as getRawMetadata(), wrapping them with callWithContext_noyield() fails with the error "Yielding is not allowed within a C or metamethod call".

Here is a simple script that tests whether a call might yield:
local catalog = import 'LrApplication'.activeCatalog ()
local LrDevelopController = import 'LrDevelopController'
local LrDialogs = import 'LrDialogs'
local LrTasks = import 'LrTasks'

local photo = catalog:getTargetPhoto ()
local n = 0

LrTasks.startAsyncTask (function ()
    for i = 1, 10 do
        n = 1
        for j = 1, 10 do 
            --catalog:getTargetPhoto ()
            --LrDevelopController.getValue ("CropLeft")
            --photo:getDevelopSettings ()
            photo:getRawMetadata ("width")
            end
        n = 0
        end
    end)

LrTasks.startAsyncTask (function ()
    for i = 1, 10 do
        LrTasks.yield ()
        if n == 1 then 
            LrDialogs.message ("n = 1!") 
            break
            end
        end
    end)
If the message "n = 1!" appears, then the SDK method has yielded.

----------
* Footnote: There are two solutions for this example.  First, the calls to getRawMetadata() (which yield) could be placed after the calls to getValue() (which don't yield). This would ensure that getValue() is called while still in Develop.  Second, the calls to getValue() could be replaced by photo:getDevelopSettings(), but that's less satisfactory, since getDevelopSettings() is extremely slow (50 ms per call), making it unsuitable for use in a polling loop (necessitated because an LrDevelopController change observer executes on the main task and can't call most SDK methods).

But the subtlety of both solutions merely illustrates the main point: How is the plugin programmer supposed to anticipate all of these situations and possible workarounds?
Photo of John R. Ellis

John R. Ellis, Champion

  • 3589 Posts
  • 928 Reply Likes

Posted 2 years ago

  • 3
Photo of John R. Ellis

John R. Ellis, Champion

  • 3589 Posts
  • 928 Reply Likes
Here's a longer list of methods I've discovered that yield:

photo:getRawMetadata 
catalog:withWriteAccessDo 
catalog:getChildCollectionSet
catalog:getChildCollections 
collectionSet:getChildren 
collection:getName 
keyword:getName 
keyword:getChildren 

This makes it practically impossible for a robust plugin with background tasks or floating panels to manipulate photos, collections, or keywords, since the user could delete or change those objects while they're being manipulated by the plugin.