SDK: Many methods yield, preventing robust plugins
[This technical note was originally published in the old feedback forum, June 4, 2016, but didn't get migrated to the new forum platform:
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?
