A word that is often thrown around (very often without definition) in the mobile industry these days is native.
It's typically used in the context of application platforms, frameworks, and ecosystems, suggesting that the one with the native label is somehow closer to the hardware, which implies (depending on which camp the labeller is sitting in) either harder to use (bad) or giving you better performance (good).
A strict definition of native code would probably involve compilation before deployment into instructions executed directly by hardware.
My assertion is that even if something is strictly native, that does not automatically mean it is any better or worse than a competing non-native equivalent.
I see many different kinds and levels of virtual machines and abstractions as you roam up and down the software stack. Some might be labelled native, other not. But each of those abstractions need to be judged and compared on their actual merits and failings, not solely by a label.
<p style="font-size:75%;">(An aside: ecosystem is an interesting choice of metaphor for the mobile application platforms. They tend to be ecosystems that involve one giant and heavily armed animal trapping thousands of defenceless small animals, making them fight against each other for survival with two paws tied together, and then finally stealing a share of all the food they collect. If that was a natural ecosystem would the small animals quickly die, or else work out how to untie their paws and keep all their food?)
Like many people I know (and possibly some of you reading), I've spent more than a few years now assembling the operating systems for many kinds of different phones and devices - some you might have seen and used, and others that you never got the chance to try.
In that time I've been lucky enough to work at many levels of the giant software stacks in those devices, from kernels and device drivers sitting bare on the hardware, through systems and servers in the middle, up to UI frameworks and applications at the top that you - with your user hat on - see and interact with directly.
If you've been a user of devices like smartphones, but not actively involved with making the software that runs them, you might not fully appreciate that these are some of the most complex systems humans have created to date.
To start with, there are a staggering number of individual processing cores in a typical device like an iPhone. Normally we just focus attention on the big and fast CPU cores, of which the latest devices typically have 2 (dual) or 4 (quad).
But often many subsystems and peripherals integrated inside the chip at the heart of the device can have full independantly-executing processing cores of their own. Integrated graphics processing units (GPUs) have multiple shader units. Modems can have multiple ARM and DSP cores. WiFi & Bluetooth peripherals can have another ARM core dedicated for running those protocols. Multimedia subsystems can have DSPs or ARM cores for audio and video processing.
Since it's the responsibility of the operating system in the phone has to tie all those pieces together and make them work as one consistent whole, that naturally makes operating systems very complex, typically needing many millions of lines of source code.
Earlier, I've had the chance to spend some time working a little bit outside the box, with a performance profiling tool that gives you the complete picture of exactly what every process, thread and module of an operating system is doing at once, and how they are interacting with each other, at a resolution of billionths of a second - which is the timescale that current systems operate at. Exploring systems and use cases with that was a very educational experience.
Every piece of software is written to work in a particular kind of environment. Part of that environment comes from the language the software is written in. Another part comes from how that language is finally executed. Yet another part is from the runtime environment it executes within. And finally there are the APIs and interfaces offered to interact with the world outside the environment - files, network, displays, etc.
All of these things are abstractions - constructed on top of some other lower level environment, (ideally) with a goal of making it easier to achieve things than it would have been if done directly with that underlying environment.
Underneath those come lower-level application environments based on C++ or ObjectiveC compiled to hardware-executed instructions, backed up with a large set of libraries and service APIs.
Below those, we typically stay with compiled C-languages, but strip away the dependencies on other services until the lowest level "user space" components depend only on something like a C library and some interfaces from the kernel.
At this point, we are still working in virtual machines - typically called threads or processes - that conveniently hide away messy details like which page in which RAM chip our memory blocks came from.
Jumping down from there we reach "kernel space". What that means in practice is that the CPU switches to a different (priviledged or supervisor) mode which gives the code running there much more control and visibility. The developers at this level still work with C or even hand-written assembler code, but they usually have a different and simpler set of abstractions to build on. Now we are underneath or outside those user-space virtual machines, able to see the "real" memory pages they are using, for instance.
It doesn't stop there. The kernel itself can be living inside a virtual machine like a hypervisor, which might route some hardware events or interrupts to be handled by a different parallel software stack unknown to the kernel.
Going down further we get to a high-level hardware view of the world, and enter a different universe of parallel blocks, clock domains, buses, caches, and other logical parts that come together to create the virtual environment executing the kernel or hypervisor.
Slightly surprisingly, hardware is often developed with software-like languages and abstractions. Due to the high financial and schedule cost of mistakes, hardware developers spend much more time simulating and testing their code before comitting it to silicon (it's always fun when hardware and software developers try to communicate and agree things - a bit like like venus and mars).
We could go further down - gates, electrons, quantum effects and subatomic particles, and finally out to the fundamental universe simulation we all think we're living in. (Woah, toooo deeeep. Emergency surface!)
Where was I? Oh yes. Levels of abstraction.
The higher you go, the more amplifying the abstractions tend to be. By this I mean that a small set of operations can cause very much action from the system underneath. Powerful, but also a little bit dangerous.
The lower you go, the more direct the abstractions tend to be. For instance, they are often concerned with controlling the state of actual pieces of hardware, or HW subsystems of the device you are running on.
Any abstraction can be designed well, or designed badly.
A good abstraction gives you expressive power to achieve a goal with less effort, in part because it handles many (lower level) problems for you. In the best case it can often do this with good or better performance than a naive hand-made solution working at a lower level.
On the other hand, a bad abstraction can hide things from you that could be easily achieved working at a lower level. Or even worse, it could totally fail to handle lower level problems for you while also leaving you unable to solve them either, due to hiding the lower levels from you.
Descriptions like native and non-native are not synonyms for good and bad abstractions.
I've used complex but poorly-performing libraries and frameworks based entirely on C++.
I've also used productive and high-performance libraries based on interpreted languages.
Flexible compiler frameworks like LLVM blur the lines further about exactly when and how the final hardware instructions are created and executed, for example allowing many specialised versions of a function optimised at run time for each different kind and value of data being processed - something that isn't feasible with ahead-of-time compilation.
Taking my own favourite area - shader programs. These are usually distributed and deployed as text-based source code in a hardware-neutral language (like GLSL). They are then compiled on demand on the device running the application into hardware instructions, which are then uploaded to a graphics processing unit and run in parallel on many tiny processors. This approach can be <i>orders of magnitude</i> faster that an equivalent C++ program run on a traditional CPU. Is that approach native?
The most important thing is to really study and benchmark an abstraction, to determine if, in actual practical use, it is well designed and bringing more value than costs.
Some of the questions to ask are:
- Can problems be solved with less effort using this abstraction than the level below it?
- Are the problems really solved, or do solutions using the abstraction actually tend to create new problems like reduced performance or increased memory consumption?
- Does the abstraction prevent features of the level below from being used? If so which ones, and is their loss a good trade for the features the abstraction brings?
- Does the abstraction offer an extension or binding route to the level below? If so, is using that still less work than entirely working at the level below? (not always the case!)
Keep your wits about you. Don't be swayed either way by something being labelled native or not. Look for the good abstractions that solve old problems for you, and help you solve new ones with less effort.
Challenge or fix or bypass the bad abstractions that claim to make 80% of the development or deployment easier, but in reality make the final 20% impossible. And watch out for giant animals.