Tuesday, August 15, 2017

A Subtle Difference between C and C++: String Literals

For the most part, C++ is a superset of C.  You can write C code, rename the file with a .cpp extension, and the compiler will compile it in C++ mode, generating essentially the same code.  In fact, the very first C++ compilers were actually C compilers with extra pre-processing.  But there are some subtle differences, and I recently ran into one that has some important implications.

In C, string literals are not constant, but in C++ they are.


For most practical purposes, string literals are constants in C.  The compiler puts them into a data segment that the code can't modify.  All the libc string functions take (const char *) parameters.  But if you assign a (char *) pointer to the start of a string literal, the compiler won't warn you about dropping the 'const' from the type, and you can write code that modifies the string (only to have it fault at run time).  In fact, the warning about dropping the 'const' is probably the reason that newer versions of C haven't changed string literals to be constant.

Why does anyone care?

I've found two specific instances where code works when compiled as C++, but not as C because of this.

You can't use string literals as the targets of case statements.


This may seem obvious, as a case statement takes an integer type, not a string.  But suppose you want to do a switch based on the first four characters of a string (after ensuring that it's at least four characters long).  Imagine the following macro:

#define STR2INT(s) ( ((s[0]) << 24) | ((s[1]) << 16) | ((s[2]) << 8) | (s[3]) )

Now you could write code like:

   switch(STR2INT(option)) {
      case STR2INT("help"):
         ...
      case STR2INT("read"):
         ...
   }

That works in C++.  But in C, the compiler complains that the case statements aren't constant expressions.  To make it work in C, you have to have a much uglier version of the macro for the case statements:

#define CHARS2INT(w, x, y, z) (((w) << 24) | ((x) << 16) | ((y) << 8) | (z))

Then the code looks like:

   switch(STR2INT(option)) {
      case CHARS2INT('h','e','l','p'):
         ...
      case CHARS2INT('r','e','a','d'):
         ...
   }

That works in both C and C++, but is a pain to write.  At least the STR2INT macro works fine in other situations where the compiler insists on constant values.

You can't write asserts based on macro names.


In large software projects, it's not unusual to have sets of macros for specific purposes.  These macros are by convention supposed to follow some project-specific format.  There even may be a defined correlation between the name of the macro and the value.  It would be nice to be able to write asserts based on the macro name to enforce those conventions.

A quick aside on asserts:

Both C and C++ now support compile-time asserts.  It used to be that you would write code that would generate a negative shift if the expression wasn't true or something like that.  When the assert failed, you would get a compile-time error that was rather confusing until you looked at the offending line.  With the new mechanism, the compiler displays whatever message you tell it.  You use static_assert(expression,"message");  In C, you have to include <assert.h> or use _Static_assert.  This was added in C11 and C++11.

So for a trivial example, suppose we have macros like:

#define VAL_7B 0x7b

Now somewhere we use those macros:

   process_value(VAL_7B);

Obviously real code would have other parameters, but this is enough for our purposes.

To have asserts based on the macro name, what appears to be a function call must also be a macro; presumably a macro wrapper around the real function call.  Consider this definition:

#define process_value(v) \
   do { \
      _process_value(v); \
   } while(0)

That's a basic wrapper, forcing a semicolon at the end of the do-while loop.  This lets us add in asserts using the '#' preprocessor operator to stringify the input parameter:

#define CHAR2HEX(c) ( (c) <= '9' ? (c) - '0' : (c) - 'A' + 10 ) // Assumes uppercase
#define process_value(v) \
   do { \
      static_assert( (#v)[0]=='V' && (#v)[1]=='A' && (#v)[2]=='L' && (#v)[3]=='_', "Must use a 'VAL_xx' macro here" ); \
      static_assert( CHAR2HEX((#v)[4]) == ((v)>>4)  , "'VAL_xx' macro doesn't match defined value" ); \
      static_assert( CHAR2HEX((#v)[5]) == ((v)&0x0f), "'VAL_xx' macro doesn't match defined value" ); \
      static_assert( (#v)[06]==0, "'VAL_xx' macro format wrong" ); \
      _process_value(v); \
   } while(0)

In C++, that works great.  In C, you just can't do that.

And here's something interesting:  Why not change the above example to look like:

#define CHAR2HEX(c) ( (c) <= '9' ? (c) - '0' : (c) - 'A' + 10 ) // Assumes uppercase
#define process_value(v) \
   do { \
      static_assert( (#v)[0]=='V' && (#v)[1]=='A' && (#v)[2]=='L' && (#v)[3]=='_', "Must use a 'VAL_xx' macro here" ); \
      static_assert( CHAR2HEX((#v)[4]) <= 0xf  , "'VAL_xx' macro with bad hex value" ); \
      static_assert( CHAR2HEX((#v)[5]) <= 0xf  , "'VAL_xx' macro with bad hex value" ); \
      static_assert( (#v)[06]==0, "'VAL_xx' macro format wrong" ); \
      _process_value( CHAR2HEX((#v)[4])<<4 | CHAR2HEX((#v)[5]) ); \
   } while(0)

This uses the value directly out of the macro name, so you can leave the value off entirely when defining the macro, right?  Yes.  But it goes further than that.  Since the above code only uses the stringification of the parameter, it never expands it.  That means it's perfectly happy if you never define the VAL_XX macros at all, which is probably not what you want.  Be sure that the wrapper macro expands the macro somewhere if you want to be sure it's actually a defined macro.

Conclusion


So if you've followed my other writing up to this point, you're probably expecting some clever hack to make this work in C.  Sorry, but not this time.  It would probably be relatively simple to add a compiler option or #pragma directive to make string constants literal in C, but gcc doesn't have this, and I'm not aware of any other compiler that does.  (Please comment if you know otherwise.)  There are plenty of tricks you could do if you're willing to use additional tools in your build process, like an additional step between the regular preprocessor and the compiler to look for extracting characters from string literals and convert them into character constants (and you could tell the compiler to use a wrapper script to do that as the preprocessor), but that's not likely to be an acceptable option.

You just can't do that in C.

Resources

This is the test file I used to be sure my above examples were correct: c_vs_cpp_example.c

Friday, August 11, 2017

Installing Gentoo Linux on a Dell Precision 5510

From time to time I realize just how slow and outdated my computer is.  Usually that's long after the corporate policy says I can order a new one, and this time was no different.  The engineering laptop is a Dell (no surprise if you know where I work), and the default model is the Precision 5510.  New computers always come with Windows installed, so my first task is to install Linux.  I figured other people with this or similar models will want to hear about what issues I encountered, so I'm documenting the process here.

The first thing I did was update the BIOS.  There have been some low-level security issues in the press recently, so I figured I should start with the latest base.  This was easy: I booted into the installed Windows, logged in, and went go to http://dell.com/support.  From there I entered my service tag, clicked on downloads, and selected the BIOS.  As I expected, the installed BIOS was a few months older than the latest one.  I'm glad I didn't try to install this under Linux, though I assume there is a way to do so.  It was a simple process of clicking the obvious choices, and then it rebooted and completed flashing.

Now the challenges began.

This is my first laptop without a CD/DVD drive (unless you go back to the floppy days).  So to boot Linux, I needed a bootable USB flash.  I had followed some very complicated instructions to set up a flash drive, but then found that I had much better results by simply taking the Gentoo live CD and using 'dd if=source_file.iso of=/dev/sdb bs=8192k' to copy it.

So it should be simple to boot from the flash drive, right?  Wrong!  Windows set up a "fast boot" where it bypasses most of the BIOS, including the part that watches for holding down F12 for the boot menu.  After some searching, I found that you need to enter "control panel" in the Windows search thing that replaces the Start menu (this is my first time to ever run Windows 10).  From there I used one of the control panels to disable fast boot.  Even with that, it took several tries to get it to recognize the F12 for the boot menu--it checks it very early in the process and moves past it very quickly.

So I booted the Gentoo USB, and started following the installation guide.  I've done this before, so I wasn't expecting too much trouble, but I did have some surprises.  First, I wiped the partition table and set it up for Gentoo.  I chose to use 1GB VFAT for the EFI partition that is also used as my /boot partition.  When everything is good, I'll only mount it to copy over new kernels when updating.  Most of the rest of the drive is my root partition, for which I selected ext4.  The last 64GB I used as a swap partition, which is excessive, but I hate running out of memory and having the OOM killer sacrifice the one thing I wanted to keep alive.

At this point, I found that I had no network.  This is my first modern laptop without an ethernet port.  It came with three different USB-C dongles that I could use, one is just ethernet, one is ethernet, video, and USB, and one is essentially a docking port with lots of ports on it.  What I quickly learned is that USB-C isn't like traditional USB.  USB-C is the connector, but it can use several different protocols over that connector, including PCI, in which case the marketing name is Thunderbolt.  None of that was working in Linux.  More on this in a bit...

So for the time being, I tried copying over stuff using a second flash stick, but that didn't get me very far.  I found an old regular USB ethernet dongle, and that worked fine.  Once I set up my kernel to boot from EFI, everything should have been mostly good, but of course it wasn't.  I had to go back and forth between booting from USB and failing with my kernel.  This was a challenge because I couldn't get the F12 menu to come up once I had wiped the Windows partition.  I ended up switching back and forth between legacy and EFI booting in the BIOS; legacy for USB and EFI for my attempt at my installed kernel.  This probably wasn't necessary, but it worked to get me past the problem.  (I needed to go into the BIOS anyway to tell it to boot the grub loader from EFI instead of Windows.)

Getting the system to boot natively was a huge pain.  Part of the problem is that after the boot completes, Gentoo clears the screen for the login prompt, so you lose the boot messages.  I decided to fix this by commenting out the getty command for tty1 in /etc/inittab.  Now I have to switch consoles to log in, but I can always see the last screenful of the boot.  I eventually got my installed kernel to work.  It turned out that I had failed to configure tmpfs, and Gentoo fails the boot process miserably if it can't put a tmpfs file system on /run.  This is what I get for configuring my own kernels--a bit of pain, but I always learn things.

So I finally dived into the problem of the Thunderbolt devices simply not working.  The main problem was again the BIOS.  It was configured with security options for Thunderbolt.  This is a good idea--without security, any device plugged into that port can do anything it wants with your computer.  But for setting up Linux, it's not a good idea, so I turned off all the security options.  I would like to turn them back on at some point, but I'll have to do some research to see if and how Linux handles Thunderbolt security.

Issues

Now I'm able to get the system up and running for the most part, but I'm still having some issues.  I'll document each one here:


Issue:

Switching from X back to a console works, but then moving back to X doesn't

Solution:

Need to load firmware as mentioned in https://wiki.gentoo.org/wiki/Intel
Switch acceleration from Sandybridge New Architecture (SNA) to UXA
Setting 'Option "DRI" "False"' works--switching is consistent, though there's an 8-10 second black screen when switching back to X.

Issue:

The trackpad works fine in the console as long as you don't want to do a right or middle click.  If you're used to using gpm, you can't do cut-and-paste operations or anything else requiring a middle or right mouse button.

Solutions:

I have not found any solution to this problem.

Issue:

X has no middle-click on the trackpad.

Solutions:

You can set the logical location of the middle button to be in the middle of the bottom of the trackpad with the synclient command.  Unfortunately, this doesn't help with gpm on the console.  I added the following script to my X startup script:

#!/bin/bash
eval $(synclient -l | egrep '(Left|Right)Edge|RightButtonAreaTop|MiddleButtonAreaRight' | sed -e 's/ //g')
if [ "${MiddleButtonAreaRight}" != 0 ]; then
    echo Middle mouse button already set up
    exit 0
fi
# The order here is critical or it will be rejected:
synclient RightButtonAreaLeft=$(( RightEdge - (RightEdge - LeftEdge) / 3 + 1 ))
synclient MiddleButtonAreaRight=$(( RightEdge - (RightEdge - LeftEdge) / 3 ))
synclient MiddleButtonAreaTop=${RightButtonAreaTop}
synclient MiddleButtonAreaLeft=$(( LeftEdge + (RightEdge - LeftEdge) / 3 ))

You can play with that to get the buttons to work however you like.  I chose to have three equal buttons, and I didn't play with the other settings, but you can look at the output of 'synclient -l' to see what all you can fiddle with.  Do note my comment about the order.  You can't have overlapping buttons, so I had to be sure to shrink the right button before defining the middle button (the left button is implicit, so you don't have to adjust it).

Issue:

The Thunderbolt devices work fine if they're connected when the system boots.  If you unplug and replug them, the system sometimes needs a kick to rescan the PCI bus:
echo 1 > /sys/bus/pci/rescan
Takes care of it usually.  However, after several repeated disconnect/connect cycles, the PCI scan fails, and it can't use the device.

Solution:

I don't do a lot of disconnecting/reconnecting, so I haven't dug any further into this.

Kernel Config

Getting the kernel config right for all the devices can be tricky.  I'm old fashioned enough that I configure my kernel with support for exactly the devices I use, avoiding modules as much as possible.  I'm noting here some of the drivers you'll want to be sure to include:

Trackpad: From dmesg, I see it's a SynPS/2 Synaptics TouchPad.

Ethernet: The dock uses a Realtek 8153 USB ethernet chip.

Sound: The WD-15 dock uses a 0bda:4014 Realtek USB audio device

Touch Screen: It has an Elan touchscreen.  Select the "HID Multitouch panels" driver under "Special HID drivers" in the kernel configuration.  It's working, but I haven't tuned it.  At some point I may want to investigate, and this looks like a promising guide:  https://wiki.archlinux.org/index.php/Calibrating_Touchscreen

WiFi: It has an Intel 8260 chip, which uses the iwlwifi driver  It also requires firmware.  I used this guide:  https://wiki.gentoo.org/wiki/Iwlwifi

Bluetooth: It has an Intel 8087:0a2b USB Bluetooth device.  I configured it so that the kernel recognizes it and is happy, but I don't have a wireless keyboard or mouse, so I may not use it anytime soon, but I'm thinking about using it for audio.  The only catch was that before it would connect to my Amazon Echo, I had to tell it, "Alexa, pair with my phone."  A volume control shows up with "alsamixer -D bluealsa."  The guide I used is: https://wiki.gentoo.org/wiki/Bluetooth

Video: It has both Intel and Nvidia graphics, but unlike some laptops, you can't just ignore the Intel and use the Nvidia graphics--you have to access the Nvidia through the Intel GPU.  So you can't install the Nvidia drivers without first installing the Intel drivers.

Flash Reader: It's a Realtek RTS525A PCI device.  It requires the kernel to be built with CONFIG_MFD_RTSX_PCI and CONFIG_MMC_REALTEK_PCI.  Unlike my old laptop, the card sticks out when inserted, so I can't leave an empty microSD adapter in the laptop.

WebCam: I haven't looked into using the webcam.