Skip to content
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

Can't catch exceptions at compile time with --os:standalone #18322

Open
exelotl opened this issue Jun 21, 2021 · 10 comments
Open

Can't catch exceptions at compile time with --os:standalone #18322

exelotl opened this issue Jun 21, 2021 · 10 comments

Comments

@exelotl
Copy link
Contributor

exelotl commented Jun 21, 2021

The combination of --gc:arc --os:standalone is viable now thanks to #16404 being fixed in devel. However, any code that relies on catching exceptions at compile time will no longer compile. This breaks a lot, for example I can't use fmt in macros anymore.

Example

static:
  try:
    raise newException(Exception, "Oh no")
  except Exception as e:
    discard

Current Output

Error: system module needs: getCurrentException

Expected Output

Program compiles successfully.

Additional Information

--os:standalone seems to be the culprit, it happens regardless of --gc setting.

$ nim -v
Nim Compiler Version 1.5.1 [Linux: amd64]
Compiled at 2021-06-21
Copyright (c) 2006-2021 by Andreas Rumpf

git hash: 40ec8184ad51ccdaa6042132dc9b37b6f242c362
active boot switches: -d:release
@Araq
Copy link
Member

Araq commented Jun 22, 2021

Why use --os:standalone, you're supposed to use --os:any.

@exelotl
Copy link
Contributor Author

exelotl commented Jun 22, 2021

--os:any seems to use stdio which increases static RAM usage by >6KB. --os:standalone makes it easy to define a custom panic handler, and ensures no runtime exception handling, which are both important features to me.

Are there ways to achieve what I want with --os:any?

@Araq
Copy link
Member

Araq commented Jun 22, 2021

I don't know, it's 2021 and I expect linkers to be able to perform dead code elimination, including for C's bizzarely bloated builtin IO facilities.

@RSDuck
Copy link
Contributor

RSDuck commented Jun 24, 2021

I don't know, it's 2021 and I expect linkers to be able to perform dead code elimination, including for C's bizzarely bloated builtin IO facilities.

I think the linkers are doing their job fine. It's just if there's a single printf or puts somewhere it pulls in a bunch of other code.

@demotomohiro
Copy link
Contributor

I compiled following simple C code with several gcc options to see if gcc's linker can remove unused C standard library code.
I used MessageBoxA windows API because it can output message without any C standard library.
Those result can be different on other platform.

#define WIN32_LEAN_AND_MEAN
#include <Windows.h>

int main() {
/*void __main() {*/
  MessageBoxA(0, "test", NULL, MB_OK);
  return 0;
}
> gcc --version
gcc (Rev2, Built by MSYS2 project) 9.3.0

This is gcc commands and sizeof generated executable file:

gcc testc.c
300,991 bytes
gcc -Os testc.c
301,027 bytes
gcc -Os -s testc.c
16,896 bytes
gcc -s -ffunction-sections -fdata-sections -Wl,--gc-sections testc.c
14,848 bytes
gcc -Os -s -ffunction-sections -fdata-sections -Wl,--gc-sections testc.c
14,848 bytes

I changed above C code so that it can be compiled without linking standard C library:

#define WIN32_LEAN_AND_MEAN
#include <Windows.h>

/*int main() {*/
void __main() {
  MessageBoxA(0, "test", NULL, MB_OK);
  /*return 0;*/
}
gcc -nostartfiles testc.c
6,211 bytes
gcc -s -nostartfiles testc.c
3,584
gcc -Os -s -nostartfiles testc.c
3,584
gcc -c testc.c && gcc -s testc.o -nostdlib -luser32
3,584
gcc -c testc.c && gcc testc.o -nostdlib -luser32
6,211
gcc -Os -c testc.c && gcc -Os -s testc.o -nostdlib -luser32
3,584
gcc -Os -flto -c testc.c && gcc -Os -flto -s testc.o -e __main -nostdlib -luser32
3,584
gcc -c testc.c -ffunction-sections -fdata-sections && gcc -s testc.o -nostdlib -luser32 -Wl,--gc-sections
3,584

GCC options:
https://gcc.gnu.org/onlinedocs/gcc/Link-Options.html#Link-Options
-s option: Remove all symbol table and relocation information from the executable.

-nostartfiles: Do not use the standard system startup files when linking. The standard system libraries are used normally, unless -nostdlib, -nolibc, or -nodefaultlibs is used.

-nostdlib: Do not use the standard system startup files or libraries when linking. No startup files and only the libraries you specify are passed to the linker, and options specifying linkage of the system libraries, such as -static-libgcc or -shared-libgcc, are ignored.

gcc can generate minimum executable file from the minimum C code only with -s and -nostartfiles option if C standard library was not used.
It seems gcc's linker can removes unused C standard library code as -nostartfiles option still link the standard system libraries.
But it cannot automatically remove the standard system startup code unless -nostartfiles or -nostdlib option is specified.
I don't know what the standard system startup does, but if it initializes C standard library, using C standard library with -nostartfiles can be unsafe.
So, if you want minimum size executable file, you have to write code without any C standard functions and add -nostartfiles to avoid the standard system startup files.

@exelotl
Copy link
Contributor Author

exelotl commented Jun 27, 2021

Thanks for looking into this. Executable size and dead code aren't exactly the problem - my target platform has plenty of ROM but not much RAM. I don't want to remove the C stdlib, I just need to avoid the I/O functions which bring in some large statically-allocated arrays.

An empty program using --os:any does call fwrite. Looking at the generated code, I've learned that this is just due to Nim's signal handler, which I can disable with -d:noSignalHandler. I was also glad to find that exceptions don't do any I/O by themselves - I can assign the unhandledExceptionHook to display the error however I want.

This means I can make --os:any work for me (though I'll probably want to look at replacing my toolchain's malloc implementation with a lighter one).

However I do wonder if there's still a use case for --os:standalone in applications that want to avoid dynamic memory allocation. This is hard to achieve with --os:any because all failed checks/assertions will allocate & raise exceptions, while I'd really just want them to call a panic function. What are people's thoughts on this?

@Araq
Copy link
Member

Araq commented Jun 28, 2021

This is hard to achieve with --os:any because all failed checks/assertions will allocate & raise exceptions, while I'd really just want them to call a panic function. What are people's thoughts on this?

There is also --panics:on which you should use. But yeah, assertions might need a patch for --os:any.

@khaledh
Copy link
Contributor

khaledh commented Dec 26, 2021

I'm having this exact same issue. I'm trying to write a kernel in Nim and I'm starting with a UEFI bootloader. I was able to get everything working with --os:standalone --mm:arc -d:useMalloc and provided my own alloc and memset/memcpy routines. Once I started bringing in strformat to use fmt I was faced with this issue Error: system module needs: getCurrentException.

If I try to switch to --os:any the linker complains about missing fwrite, fflush, exit, strlen, and __imp___acrt_iob_func. The latter is the stderr definition from the mingw crt (UEFI requires building a PE executable, not ELF), as Nim seems to require stderr for showErrorMessage. The call chain is nimFrame -> callDepthLimitReached -> showErrorMessage2 -> showErrorMessage -> writeToStdErr -> rawWriteString(stderr, ...).

Is there a way to avoid the dependency on those stdio routines in --os:any? I tried to provide my own errorMessageWriter as it seems to bypass writing to stderr, but obviously this is a runtime check, not a compile-time one, so the reference to stderr and friends is still emitted.

@Araq
Copy link
Member

Araq commented Dec 30, 2021

nimFrame should disappear with -d:release or maybe -d:danger.

@khaledh
Copy link
Contributor

khaledh commented Dec 30, 2021

Good to know!

I actually got myself unblocked earlier by providing stubs to the missing functions (they weren't being called anyway). I also realized that I need -d:release or -d:danger to get interrupts working properly (I think the default build mode interferes with the stack frame structure required by interrupts). Now I can remove those stubs.

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

5 participants