Conditional Compilation in MonkeyC

I'm curious as to whether anyone else has hacked together a conditional compilation setup for MonkeyC.

It seems like something that would be nice to have built-in to MonkeyC since there's such a priority on small, efficient code; rather than have debug code, etc wrapped in feature flags, it's preferable to have it excised entirely.

I've written a Python script that hunts for the following tags // [strip:begin <tag>] and // [strip:end <tag>] and conditionally comments/uncomments code between those markers.

This allows me to quickly add remove logging/test fixtures in my test and release builds.

Anyone else doing something similar? Would there be any interest in me publishing this code? Or is this a bad idea for some reason?

Curious what folks thoughts are here...
  • a simple inline #ifdef type construct would still be really nice to have. I find for most conditional compile situations I deal with, it would be a cleaner interface, than spreading code around through barrels and and jungles. I agree major functionality items like maps work great for barrels and jungles where you are swap out nearly completely different code paths. But if all you need is a few lines of code that work differently between two devices, the current options make things messy.

    The whole exclude thing definitely feels backwards to me compared to to the include construct. Effectively having to have a list of excludes for every device if you only want to include code for one device gets really ugly.

    Thankfully on newer devices there is a lot of memory, but even on the 935 I've had issues. Anything older than that can be very constraining.

    Good news is there are options that didn't always exist.
  • ekutter great point. I think I understand why the system of excludes was chosen. It lends itself naturally to including everything by default.

    However, this system breaks down when you only want to include things for one or two targets. Then when you have excludes along multiple dimensions, such as screen size/shape, font family, CIQ1/2, small memory versus large memory, the number of exclude annotations you have to write is a nightmare. Yes, it can be done in a logical way using locals, but it's still pretty annoying and hard to get right the first time you try.

    The worst is when you have to start writing different code for an individual device or twice, not a whole family.

    Luckily in most cases, if you get your excludes wrong, you will end up with duplicate or missing symbol definitions.

    I would love to have inline #ifdef / #ifndefs. That would solve all my maintenance problems instantly.

    I probably should've written that tool already.

    The current system of annotations and symbols is simple and elegant. I understand the design goals. But when you want flexibility, then you have to apply it in a very complicated way.

    ---

    EDIT: jim_m_58 I'll also clariffy that it's not just about "saying no to new features". That's easy to do when you're maxed out on CIQ 1. It's about the fact that adding new features for CIQ 2 or CIQ 3 can break your existing CIQ 1 build, if you code things in a maintainable way.... I realize this is not a problem if you leave plenty of overhead, which I admit I rarely do.
  • I would love to have inline #ifdef / #ifndefs. That would solve all my maintenance problems instantly. I probably should've written that tool already.


    Why would you need to write a tool? If you don't already have a C/C++ compiler installed, you can get a free C/C++ preprocessor pretty easily.

    https://github.com/facebookarchive/warp
    https://github.com/danmar/simplecpp
    http://mcpp.sourceforge.net/

    The problem becomes trying to get the build tools to run the preprocessor for each device before compiling and bundling.
  • Flowstate I'm not sure you read my article on the subject on the development blog but if you use jungles (or build overrides) and combine it with inheritance then you have an elegant solution that's maintainable: I try to avoid writing the same code line twice and I support all device flavors in my apps, getting the most out of memory on all devices, using the newest features where available. (ok I have a bit of memory overhead because of the inheritance and the extra class, but I think the maintenance advantage you gain by coding in this way is more than worth it).

    I'm not a fan of conditional compilation and not even a fan of excluded annotations (although I use the latter on occasion), imho both conditional compilation and excluded annotations lead to polluted unreadable spaghetti code which is something I don't like.
  • Travis.ConnectIQ yeah, silly me. I did consider that but I guess I thought a short awk script or something would be better than using the preprocessor, but I'm not sure why. I mean, I already looked up the stackoverflow answer for running the C preprocessor in standalone mode a couple of times....

    Another concern is that I would like a solution that works with the existing build system as far as build targets/exclude annotations goes, but I'm not sure if that's possible (although I haven't looked into it or thought about it hard enough). I don't know if it's possible for a 3rd party tool to "understand" .jungle files the way the monkeyc compiler does, or for the monkeyc compiler to pass that information along to a 3rd party. I guess what I'm looking for is a "custom build step" (pre or post), similar to Visual Studio, but I'm guessing that isn't possible.

    peterdedecker thanks for the advice. Yes, I do think conditional compilation is inelegant. I mean, there's a reason C# doesn't even have a preprocessor (except for the very limited handling of #defines). I like elegant code, but in this case I am not willing to take the memory overhead hit.

    I just don't like the idea that adding a feature to app X for device A can cause device B to crash, when B's feature set has been frozen for quite some time. I guess it's my fault for always trying to squeeze too many features into older devices. I mean, I realize that it's more important to make apps that people want as opposed to apps with as many features as possible....

    I'd be happy to trade code size for maintainability if more memory was available, but especially with data fields, I feel like there just isn't enough breathing room. There's so much overhead like app settings, resource list (if you load > 0 resources at run time), etc. I realize most or all of those things are necessary (although as I mentioned before, I wish the properties dictionary could be unloaded when not needed, say after init), so I'm always looking for other ways to save memory.
  • Total newbie here, but I was writing pretty much exactly code like yours. To log interesting stuff only in debug mode. Soon did I find out I have to leave empty functions for release build. That seems really odd.

    Isn't there a conditional compiliation even now, all 6 years later?

    I'm now experimenting with "Prettier monkey C" which has code optimizer, maybe it could remove the empty functions and their call statements. It advertises removing dead code...

  • Isn't there a conditional compiliation even now, all 6 years later?

    Here's the half-baked, basic solution that I use for some of my apps. It isn't updated for VS Code tho.

    EDIT: There's a couple of other solutions in the thread which might be better, but I haven't tried them.

    forums.garmin.com/.../solution-for-line-by-line-mc-conditional-compilation

    I'm now experimenting with "Prettier monkey C" which has code optimizer, maybe it could remove the empty functions and their call statements. It advertises removing dead code...

    I don't think it'll work for your use case since it operates before exclusions are processed by the compiler.

  • If you already use prettier, then just add (:inline) on the empty functions, it'll replace them with {} which doesn't add a byte to the bytecode. Another thing I use with prettier are boolean constants, ie:

    (:debug) const LOG = true;
    (:debug) const DEBUG = true;

    (:release) const DEBUG = false;
    (:release) const LOG = false;

    (:inline)
    function log(msg as String) as Void {
      if (LOG) {
        logAlways(msg);
      }
    }

    function logAlways(msg as String) as Void {
      System.println(Time.now().value() + " " + msg);
    }
  • Hey thanks, it almost works like this:

    import Toybox.System;
    import Toybox.Lang;
    
    (:inline,:debug)
    function log(message as Lang.String) as Void {
        System.println(message);
    }
    
    (:inline,:release)
    function log(message as Lang.String) {}
    

    But if I call it like this: 

    log("Did something important");

    It optimizes it to this on release:

    {
        var message = "Did something important";
    }

    And on debug to this:

    {
        var message = "Did something important";
        System.println(message);
    }

    Annoyingly close! It still leaves that variable allocation there, maybe I try to report this on the prettier Github extension page as bug.

  • You can report it here: forums.garmin.com/.../big-update-to-prettier-extension-monkeyc

    I don't think it's a bug, rather as a feature request:

    "Remove" unused function arguments when inlining