How to fortify code with runtime checks?

Let’s strengthen code with runtime checks

Aaina jain
Swift India
4 min readJun 18, 2018

--

Compile-time type checking is not possible in some scenarios where run times type check takes place. Like reading data from the network (at that time source code is not aware of data), taking input from user not satisfying requirement and overflow issues due to arithmetic operations etc.

Swift provides Assertions to be aware of these issues.

Before deep diving into Assertions, let’s have a look on newly introduced Swift compilation mode and Swift optimization level.

Swift Compilation Mode

Swift Compilation Modes
  • Debug builds: Incremental — Swift supports building targets incrementally i.e. not rebuilding every Swift source file in a target when a single file is changed. Every file dependent on the modified file will be rebuilt.
  • Release builds: Whole Module — This option optimizes all files of a module as a whole in a target together and enables better performance. It can perform optimizations like function inlining and function specialization.
whole module optimization

Swift optimization level

Swift provides four different optimization levels:

  • -Onone: It is basically for normal development i.e. debug builds. It performs minimal optimizations and preserves all debug info. It’s advisable to always use incremental compilation for this mode.
  • -O: This is meant for production code. The compiler performs aggressive optimizations that can drastically change the type and amount of emitted code. Debug information will be emitted but will be lossy.
  • -Ounchecked: This is a special optimization mode meant for specific libraries or applications where one is willing to trade safety for performance. The compiler will remove all overflow checks as well as some implicit type checks. This is not intended to be used in general since it may result in undetected memory safety issues and integer overflows. It should be only used if you have carefully reviewed that your code is safe with respect to integer overflow and type casts.
  • -Osize: This is a special optimization mode where the compiler prioritizes code size over performance. This mode works in whole-module as well as in single-file compilation, whereas whole-module mode gives the best optimization results.
Swift Optimization Levels

NOTE: Optimization level and compilation mode can be set in build settings of the project.

Assertions:

Whenever we need to check our code against a condition that is expected to be met, we can use assertion and exception will be thrown.

Standard Swift library comes with five assertion functions:

  1. assert(_:_:file:line:)
  2. assertionFailure(_:file:line:)
  3. precondition(_:_:file:line:)
  4. preconditionFailure(_:file:line:)
  5. fatalError(_:file:line:)

Let’s explore each function in detail:

  1. assert(): This method is similar to traditional C — style assert with an optional message. It should be use only for internal sanity checks that are active during testing but do not impact performance of shipping code. If condition evaluates to false`, stop program execution in a debuggable state after printing message.

Example: For a kid to take admission in school minimum required age is 3 so we can check this condition here:

2. assertionFailure(): If you don’t have condition to evaluate, you can use assertionFailure(). This method indicates that an internal sanity check failed. It should be use to stop the program, without impacting the performance of shipping code, when control flow is not expected to reach the call.

Example: Unwrapping nil value can cause app crash. It’s good to use assertionFailure() with guard return statement.

3. precondition(): This method checks a necessary condition for making forward progress. It’s almost similar to assert() while it works for release build too.

4. preconditionFailure(): It’s almost similar to assertionFailure() while it works for release build too.

Return type of preconditionFailure() is Never which indicates that function will never return and will stop the execution of app. If the function is expected to return a value then in case of preconditionFailure you are not forced by the compiler to write a return statement whereas in case of assertion failure you are required to add one.

5. fatalError(): It is similar to preconditionFailure() in the fact that it will also get executed for release build and takes a message as input. Fatal errors are a easy way to quit the execution of the program regardless of the build’s optimizations mode. When a project is compiled for an Unchecked Release (-Ounchecked), it will assume that neither assertionFailure nor preconditionFailure will ever be called.

Representations of function in all builds

Note: “**” Optimizer may assume that this function is never called.

References:

Conclusion:

Adopting assertion is a good practice. You should be mindful of using the correct function for appropriate build. A small mistake can lead to a crash in production app which can affect the end user.

Thank you for reading and I look forward to your feedback.

You can catch me at:

Linkedin profile: Aaina Jain

Twitter: __aainajain

--

--