UnexpectedTypeException: Expected a Toybox.Lang.Number or WatchUi.BitmapResource

One of my users reported this error, but unfortunately I don’t have a stack trace to go on.
I’m hoping to tap into the swarm intelligence here - does anyone know of an SDK function that accepts a parameter which can be either a Number or a BitmapResource?
I’ve checked my own code and don’t see any instances of this.

  • If you're asking to see if it's documented, then there are similar examples. i.e String.substring accepts null.

  • If you're asking to see if it's documented, then there are similar examples. i.e String.substring accepts null.

    Yes, that’s exactly what I meant. In a similar manner, the documentation for Bitmap.initialize and Bitmap.setBitmap should clearly state that BufferedBitmap can only be used on devices running CIQ 4.0.0 or later.

    https://developer.garmin.com/connect-iq/api-docs/Toybox/WatchUi/Bitmap.html#initialize-instance_function

    https://developer.garmin.com/connect-iq/api-docs/Toybox/WatchUi/Bitmap.html#setBitmap-instance_function

    But even if it was well-documented, it may have slipped past me. For me, the real issue is that the compiler allowed passing in a BufferedBitmap, and the simulator accepted my workaround wrapper without complaints. The problem only surfaced on a real user device, which made it much harder to catch.

  • yes, this is another topic we discussed in the last 2 weeks, that the compiler could catch many errors at compile time that it passes now, and we can only see it as a runtime error...

  • that the compiler could catch many errors at compile time that it passes now, and we can only see it as a runtime error...

    If you're referring to the bug report you opened where you said the compiler should be able to check that you called an unsupported function on an old device (in the absence of a has check), I don't think this is exactly the same.

    For one thing, calling a function that doesn't exist on a given device fails in a well-defined manner, and it should fail in both the sim and the real device.

    For another thing, in that case, it's should be well-documented that the function in question is unavailable on certain devices

    In this case:

    - the simulator incorrectly behaves as if the code would work

    - the behaviour on the real device varies. On a fenix6pro, a "strange" exception is thrown from internal code. On an fr935, no exception is thrown, but the code doesn't work properly. This difference can be explained by the fact that fr935 probably doesn't even know about the :bitmap option in question, so from it's pov it probably looks like you just forgot to specify any option at all (i.e. you forgot to specify :rezid)

    - it's not documented which options / values are available for which devices. Looking past the :bitmap option, I don't know which of the other options (such as :locX, :locY, etc.) are unavailable for certain devices. I can guess that everything but :rezId is unavailable for fr935, but it would be nice if Garmin would just tell us

    However, what the 2 cases do have in common is that Garmin seems to be reluctant to return a compile error for code that could work on 1 device, but fail on another, instead preferring to allow the code to fail at runtime (one way or another).

    Again, we can see why this would be the case: it's not possible for the compiler to determine in all cases whether a given line of code will execute on a given device, without actually running it. Again, a simple example would be for the app to get the CIQ version at runtime and gate functionality based on it. Now you will say this is bad code, but there are situations where devs have used this, and where veteran devs have actually recommended it. Even more to the point, you can't design a compiler that only works well for "good" code (by some arbitrary standard).

    For example, given that BufferedBitmap *apparently* cannot be used to create a Bitmap for CIQ < 4, how exactly would you gate this functionality in practice? I can think of a few ways:

    1) annotate different versions of the same function and hardcode exclusions in monkey.jungle

    2) check the CIQ version at runtime

    3) use a has check on BufferedBitmap to see if it has getWidth and getHeight

    I'm sure we could think of other things.

    How about my guess that fr935 does not support the :bitmap option at all for the Bitmap constructor? Handling that is a similar problem as above.

    Ofc, the compiler could take an approach that whenever you build for a given device, it assumes that every line of code (which isn't explicitly excluded via annotations) will run on that device, and emit errors accordingly, when code tries to call functions or pass in options that don't exist. But this could lead to many spurious errors. Even if they're warnings instead of errors, we know that devs don't like to see spurious warnings either.

    Tbf Garmin takes the opposite approach with deprecated functions: when you call a deprecated function, Garmin emits a warning even if the function is not deprecated on the device you are currently building for. It's a similar root cause tho: Garmin can't be sure that you won't be also calling this function on a newer device that you're building for.

  • I'm sure we could think of other things.

    I've implemented a custom BufferedBitmapDrawable as a solution instead of using Bitmap directly:

    https://github.com/TheNinth7/ohg/blob/main/source/user-interface/sitemap-menu/menu-items/switch/BufferedBitmapDrawable.mc

    For CIQ 4.0.0 and later, it works with the standard BufferedBitmap.
    For earlier versions, I’ve created a LegacyBufferedBitmap that adds the missing getWidth() and getHeight() methods required by BufferedBitmapDrawable to properly calculate locX and locY when layout constants are used.

    https://github.com/TheNinth7/ohg/blob/main/source/user-interface/sitemap-menu/menu-items/switch/LegacyBufferedBitmap.mc

    To distinguish between the two at runtime, I check for the existence of createBufferedBitmap.

  • Yeah I saw that code and I understood that from your post. I also tried something similar in my testing, to try to replicate your situation.

    This only proves my point that devs have many ways to gate device-specific functionality, and we can probably think of more than zero ways which would be hard or impractical to detect at compile time.

    In this specific example, the compiler would have to notice that you checked for the existence of createBufferedBitmap (only available on CIQ 4+ devices), then to note that you used LegacyBufferedBitmap and passed it in to Bitmap.initialize() (via the :bitmap option).

    iow, the compiler would have to be able to make inferences about what code is executed that would typically only be possible by running your code.

    I'm saying that the compiler isn't going to be smart enough to know - in all cases - whether some line of code runs on a given device, in order to catch this kind of problem at compile time.

    I think in this case, the best thing that can be done is:

    - document the options / values / types for Bitmap.initialize / Bitmap.setBitmap properly (there are in fact other places in the documentation which explain that some type or value is only available starting with a certain CIQ )

    - change the simulator so it fails appropriately when an unsupported option / value / type is used

  • My guess is that this doesn't work in Monkey C, because:

    1. It's not defined as an interface

    2. The code we write would not call "super".getWidth() but Bitmap.getWidth(). Though it might not be the same with native code, though it probably is similar, so if there's no such method in Bitmap, it doesn't help that your object is of a child class that implements it 

  • I think you're misunderstanding the problem completely.

    Actually, the-ninth's approach *does* work "in Monkey C", because it works for both of us in the simulator. If you subclass BufferedBitmap on an old simulated device so it implements getHeight / getWidth, it becomes possible to create a Bitmap from a BufferedBitmap.

    As I did argue and will argue again, it shouldn't work, for reasons that have nothing to do with the Monkey C language. (It shouldn't work because clearly the device has an old API where Bitmap.initialize doesn't support BufferedBitmap, but the simulator doesn't properly reflect that. The sim's implementation of Bitmap.initialize initially accepts a BufferedBitmap, but throws an error once it "realizes" it's been given an old version of BufferedBitmap which doesn't have all the functionality it needs)

    The real problem is not that the approach doesn't work on a real device, but that it does work in the sim.
    The sim isn't properly reflecting limitations of old devices wrt to Bitmap.initialize().

    2. The code we write would not call "super".getWidth() but Bitmap.getWidth(). Though it might not be the same with native code, though it probably is similar, so if there's no such method in Bitmap, it doesn't help that your object is of a child class that implements it 

    I'm not even sure you understand the approach. The issue is not Bitmap.getWidth/getHeight but BufferedBitmap.getWidth/getHeight

    We have an instance of BufferedBitmap (call it bufferedBitmap) which is being passed into Bitmap.initialize via the :bitmap option.

    This should be equivalent (when everything is working) to calling bitmap.setBitmap(bufferedBitmap) (where bitmap is an existing instance of Bitmap)

    [1/x]

  • 2. The code we write would not call "super".getWidth() but Bitmap.getWidth(). Though it might not be the same with native code, though it probably is similar, so if there's no such method in Bitmap, it doesn't help that your object is of a child class that implements it 

    This doesn't really make any sense. The code we're talking about is something like this

    In this example, it doesn't matter what class x is, so long as it has getWidth and getHeight methods.

    Ofc there are API functions which apparently use instanceof to perform run-time type checking [which is why it's even possible to say "expected an X or Y" at runtime]

    [2/x]

  • Ofc there are API functions which apparently use instanceof to perform run-time type checking [which is why it's even possible to say "expected an X or Y" at runtime]

    Let's pretend that we're calling a version of setBitmap which does support BufferedBitmap. I would expect the code to look like this:

    In this case, it doesn't matter whether x is literally a BufferedBitmap or a subclass / child class of BufferedBitmap. The instanceof expression will be true either way. This is a cornerstone of OOP: if class Y inherits from class X, then an instance of Y is an instance of X [this is referred to as the "is-a" relation between child and parent classes.]

    I don't even know of a way at runtime (or compile-time) that you could check that an object o is an instance of X and not Y [where Y is a subclass of X].

    Btw it also doesn't make sense to say that the error is happening in native code. Native code is C [or maybe C++] so it wouldn't be giving us errors about calling Monkey C functions or referring to Monkey C runtime types. I assume that Monkey C code calls native code (as in calling a native C function), but native code probably can't call Monkey C code in the way that you're suggesting (as in looking up a function like getHeight on an instance of a class, instead of allowing Monkey C code to do the lookup normally.) I would question whether native code can call Monkey C code at all (given that Monkey C code runs in a sandboxed VM), but either way I doubt that's what's happening in this case.

    When I tried out the initial repro of passing in a normal BufferedBitmap to Bitmap.initialize() in the sim the stack trace would refer to internal MC files in the API. [unfortunately when I try it now, the filenames are obscured]:

    [last time I tried this, the setBitmap and initialize stack frames apparently pointed to paths on a Garmin dev computer]

    I'm still fairly sure this is Monkey C code that's running, not native C/C++ code.

    1. It's not defined as an interface

    This also doesn't matter. An interface is something that only exists at compile time afaik. I don't think you can use instanceof on an interface. But even if you could, this is a red herring for the reason I mentioned above. Defining something as an interface doesn't give you more functionality at runtime, it's just a way to apply certain type restrictions at compile time.

    --

    The real problem is there's no good reason the workaround should work at all in practice (it has nothing to do with language constraints), as I painstakingly tried to argue above.

    1) The history of Bitmap.initialize / Bitmap.setBitmap is that BufferedBitmap was not always supported as an input (e.g. using the :bitmap option in initialize). In the oldest devices, you couldn't even initialize a Bitmap from a BitmapResource, you had to pass in a resource id - in this case, the :bitmap option didn't even exist

    2) Based on 1 and the behaviour exhibited in this thread, it is almost certain that fenix6pro does not support passing in a BufferedBitmap to Bitmap.initialize's :bitmap option; same goes for setBitmap. Indeed, the error message "Expected a Toybox.Lang.Number or WatchUI.BitmapResource" (which is almost certainly coming from setBitmap, called by initialize), seems to indicate that setBitmap was expecting a resource ID (Number) or BitmapResource. I mean there's not much more it could say other than "I don't support BufferedBitmap" but ofc we can't really expect that kind of specific error, for several reasons.

    3) If you really think about it, it makes zero sense that a an old implementation of Bitmap.initialize / Bitmap.setBitmap would accept a BufferedBitmap, but only if you could somehow modify it to add functionality that only existed "in the future" (in newer implementations). The simplest and most reasonable explanation here is that the old implementation doesn't really support accepting a BufferedBitmap in the first place. Nobody would (or could) implement an API to relies on missing behaviour that will be added in the future, unless they have a time machine and/or they really hate their devs.

    The root cause of all this confusion is that the simulator incorrectly implements more Bitmap.initialize() functionality than is actually supported on older devices. This leads the dev down the rabbit hole of:

    - passing in BufferedBitmap to Bitmap.initialize and getting a misleading error message (failed to call getHeight / getWidth) in the sim

    - thinking that all that's needed is to provide getHeight / getWidth to get it to work (but again, if you really think about it, that doesn't make any sense)

    - implementing the above workound and seeing it work in the sim

    - running the code on the real device and noticing that it doesn't work at all

    What the simulator should do:

    - for fenix6pro, which supports :bitmap on Bitmap.initialize, but not BufferedBitmap as a type for :bitmap - throw the exception as mentioned in the thread title (expected Number or BitmapResource)

    - for fr935 - completely ignore :bitmap (not great behaviour, but it would match what's on the real device). To be better than the real device, it could also complain that :rezId is required

    [3/3]