Skip to content

Casting a negative char to an int results in a positive number #6010

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
6 tasks done
MichaelBrunn3r opened this issue Apr 23, 2019 · 12 comments
Closed
6 tasks done

Casting a negative char to an int results in a positive number #6010

MichaelBrunn3r opened this issue Apr 23, 2019 · 12 comments

Comments

@MichaelBrunn3r
Copy link
Contributor

MichaelBrunn3r commented Apr 23, 2019

Basic Infos

  • This issue complies with the issue POLICY doc.
  • I have read the documentation at readthedocs and the issue is not addressed there.
  • I have tested that the issue is present in current master branch (aka latest git).
  • I have searched the issue tracker for a similar issue.
  • If there is a stack dump, I have decoded it.
  • I have filled out all fields below.

Platform

  • Hardware: ESP-12E
  • Core Version: 0dd6549
  • Development Env: Arduino IDE
  • Operating System: Windows

Settings in IDE

  • Module: Nodemcu
  • Flash Mode: ?
  • Flash Size: 4MB
  • lwip Variant: v2 Lower Memory
  • Reset Method: nodemcu
  • Flash Frequency: 40Mhz
  • CPU Frequency: 80Mhz
  • Upload Using: SERIAL
  • Upload Speed: 115200

Problem Description

Casting a negative char to an int results in a positive number. The results differ depending on the platform.

Testing on device

Arduino Sketch

#include <Arduino.h>

void setup() {
  Serial.begin(115200);
  Serial.println();
  
  char c = -61;
  if(c != -61) { Serial.print("Char is not -61, but "); Serial.println((int)c); }
  else Serial.println("Char is -61");
  
  int i = c;
  if(i != -61) { Serial.print("Int is not -61, but "); Serial.println(i); }
  else Serial.println("Int is -61");
}

void loop() {
  delay(100);
}

Debug Messages

Running on Arduino IDE, Arduino Uno

Char is -61
Int is -61

Running on Arduino IDE, Nodemcu ESP-12E

Char is not -61, but 195
Int is not -61, but 195

Testing on host

Cloned newest release. Added file to TEST_CPP_FILES. Used make test to run.

Catch test file

#include <catch.hpp>
#include <iostream>

TEST_CASE("get char") {
    char c = -61;
    if(c != -61) std::cout << "Char is not -61, but " << (int)c << std::endl;
    else std::cout << "Char is -61" << std::endl;
    
    int i = c;
    if(i != -61) std::cout << "Int is not -61, but " << i << std::endl;
    else std::cout << "Int is -61" << std::endl;
}

Debug Messages

Char is -61
Int is -61
@bertmelis
Copy link

int is 32 bit, char is 8 bit. The 2's complement of -61 is to 195.

It's not a bug, now google and learn.

@MichaelBrunn3r
Copy link
Contributor Author

Char is signed, int is signed. Char goes from -128 to 127, int from -2,147,483,648 to 2,147,483,647. A char can be perfectly represented in an int. Casting a smaller (byte size) signed type to a bigger signed type results in a signed value. At least thats how (almost) all computer languages work. As you can see the test on the Arduino Uno and on the host work as expected. Only the ESP12E is the odd one out

@MichaelBrunn3r
Copy link
Contributor Author

Casting a negative int to a long also works as expected. Only char doesnt. I suspect the char is implemented as a uint8_t on the ESP12

@earlephilhower
Copy link
Collaborator

GCC gives a warning when you compile this. That's the root of your problem, you're not really doing a comparison like you think you are in the first case. Please look at GCC's typing and condition checking.

/home/earle/Arduino/sketch_apr23a/sketch_apr23a.ino: In function 'void setup()':
/home/earle/Arduino/sketch_apr23a/sketch_apr23a.ino:7:14: warning: comparison is always true due to limited range of data type [-Wtype-limits]
     if(c != -61) Serial.printf("Char is not -61, but %d\n", (int)c);
              ^

@MichaelBrunn3r
Copy link
Contributor Author

Sorry, I'm lost on this one. A char is perfectly able to represent negative numbers. Also, how does that explain the different results on the ESP, Uno and the host?

@MichaelBrunn3r
Copy link
Contributor Author

MichaelBrunn3r commented Apr 23, 2019

To make my point even more clear:

Sketch

void setup() {
  Serial.begin(115200);
  Serial.println();
  
  char c = -62;
  unsigned char u = -62;
  Serial.println(c == u);
}

Output:

  • ESP12: 1
  • Arduino Uno: 0

This shows that a signed and unsigned char are equal on an ESP and not equal on an Arduino Uno. Mybe this is a Arduino IDE issue?

@MichaelBrunn3r
Copy link
Contributor Author

Even earlephilhower's comment should prove this. GCC is warning that a char can never be negative. Again, that only is the case if it is implemented as unsigned. But that is different from the Uno

@bertmelis
Copy link

https://stackoverflow.com/questions/436513/char-signed-char-char-unsigned-char

It's implementation-defined.

@MichaelBrunn3r
Copy link
Contributor Author

Thx, thats the answer I was looking for. So the Uno implements a plain char as a signed char and the ESP as a unsigned char. I changed the second Sketch to this:

void setup() {
  Serial.begin(115200);
  Serial.println();
  
  signed char c = -62;
  unsigned char u = -62;
  Serial.println(c == u);
}

Now the ESP and Uno give the same result.
But this would mean everywhere in this repo where a plain char is used, the code may not be portable between ESP and Uno

@MichaelBrunn3r
Copy link
Contributor Author

Example:

class MockStream : public Stream {
  private:
    String mStr;
    const char* mCstr;
  public:
  MockStream(String str) {
    mStr = str;
    mCstr = mStr.c_str();
  }
  
  virtual int available() {
    if(*mCstr == 0) return 0;
    long charsRead = mCstr - mStr.c_str(); 
    return mStr.length() - charsRead;
  }
  virtual int read() {
    return *mCstr == 0 ? -1 : *mCstr++;
  }
  virtual int peek() {
    return *mCstr;
  }
  virtual size_t write(uint8_t) {
      return 1;
  }
};

void setup() {
  Serial.begin(115200);
  Serial.println();
  
  MockStream stream("[äöü]");
  Serial.println(stream.readString());
}

Output:

  • Uno: []
  • ESP: [äöü]

@devyte
Copy link
Collaborator

devyte commented Apr 24, 2019

Plain char is not guaranteed to be portable across different platforms. The signedness of char is implementation defined, and has always been. Traditionally, i.e. Historically, older compilers used signed char as default, because the ascii table only went up to 127. Then there was the extended ascii table which went up to 255, so that habit changed to unsigned char, mostly with 32bit cpus, and then spread back to uCs.
If you need portability, don't rely on implementation defined types, but use uint8/int8 etc to make sure you get what you think.
Also, don't rely on implicit casts, the compiler warning is there for good reasons.

@MichaelBrunn3r
Copy link
Contributor Author

MichaelBrunn3r commented Apr 24, 2019

Thank you all for explaining this to me. I come from Java and never had that issue with C/C++ until now (seems that in embedded you have to be WAY more careful). I thought all primitive types were defined in C/C++, turns out only char is not (kinda a trap if you ask me :)).

Just a couple of points to end this:

  • I guess the Arduino Library relies on unsigned chars. This was not clear, there even is the 'byte' typedef in Arduino.h, which is defined as an unsigned char
  • Even though the Library is relying on the same type, the devices are not. As I have seen, Uno uses signed char, and ESP unsigned.
  • My original issue was that Stream.h was acting different on host and ESP (shown in the MockStream Sketch). From what I have learned, I have to pass unsigned chars to the library, but I passed plain chars

Changing the sketch to this fixed my issues:

class MockStream : public Stream {
  private:
    String mStr;
    const char* mCstr;
  public:
  MockStream(String str) {
    mStr = str;
    mCstr = mStr.c_str();
  }
  
  virtual int available() {
    if(*mCstr == 0) return 0;
    long charsRead = mCstr - mStr.c_str(); 
    return mStr.length() - charsRead;
  }
  virtual int read() {
    return (unsigned char)*mCstr == 0 ? -1 : *mCstr++;
  }
  virtual int peek() {
    return (unsigned char)*mCstr;
  }
  virtual size_t write(uint8_t) {
      return 1;
  }
};

Now Uno, ESP and host do exactly the same.
(Also, if you have the time to answer this, WHERE does the compiler ever throw that error, neither using Make on host nor the Arduino IDE throw that error. I guess you compiled the .ino file in the command line with gcc? Never done that, but I guess I should have a look at it)

earlephilhower added a commit that referenced this issue Apr 24, 2019
Force GCC to run with -funsigned-char during host tests  to make
the PC match the default behaviour used by the xtensa GCC port.

As noted in #6010.  Thanks @MichaelBrunn3r
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants