py/objint,py/binary: Add int.to_bytes(signed) parameter, add common overflow checks.#16311
Conversation
77160cc to
0fb050a
Compare
|
Code size report: |
c84ab58 to
88b516a
Compare
88b516a to
140c2c9
Compare
4faf739 to
b3b3d4e
Compare
|
Only two things left here...
|
Ah OK, there's a whole section about stack usage in the windows port README so I'm guessing we're just running out of stack. |
Had a quick look into why stm32 got bigger while all the other bare metal ports got smaller. Didn't find anything unusual: no significant new symbols are being linked compared to master. Several functions shrunk as they now call a common helper. I think maybe they just didn't shrink as much as on the other bare metal ports... Diff of linked symbol sizes--- ./build-PYBV10-master/firmware.syms 2025-03-04 15:59:56.145991980 +1100
+++ ./build-PYBV10/firmware.syms 2025-03-04 16:00:08.656797386 +1100
@@ -5948,0 +5949 @@
+00000018 t allowed_args.0
@@ -6199 +6200 @@
-00000264 t compile_scope_inline_asm
+00000260 t compile_scope_inline_asm
@@ -6503 +6504 @@
-00000970 t emit_inline_thumb_op
+00000978 t emit_inline_thumb_op
@@ -7326 +7327 @@
-000000d8 t int_to_bytes
+0000005c t int_to_bytes
@@ -7770,3 +7771,3 @@
-0000002a T mp_binary_set_int
-00000114 T mp_binary_set_val
-0000008c T mp_binary_set_val_array
+0000005a T mp_binary_set_int
+000000f2 T mp_binary_set_val
+0000006e T mp_binary_set_val_array
@@ -8329 +8330 @@
-00000042 T mp_obj_exception_attr
+00000040 T mp_obj_exception_attr
@@ -8373 +8374 @@
-00000016 T mp_obj_int_to_bytes_impl
+00000114 T mp_obj_int_to_bytes
@@ -8521 +8522 @@
-0000091c T mp_qstr_const_hashes
+00000922 T mp_qstr_const_hashes
@@ -8523 +8524 @@
-0000048e T mp_qstr_const_lengths
+00000491 T mp_qstr_const_lengths
@@ -8525 +8526 @@
-00001250 T mp_qstr_const_pool
+0000125c T mp_qstr_const_pool
@@ -8891 +8892 @@
-000000d2 T mpz_as_bytes
+0000009e T mpz_as_bytes
@@ -9768 +9769 @@
-00000084 t set_aligned
+00000080 t set_aligned
@@ -10078 +10079 @@
-0000007c t task_attr
+00000078 t task_attr
@@ -10216 +10217 @@
-00000200 t uctypes_struct_attr_op
+00000204 t uctypes_struct_attr_op(Files generated with I don't see much we can do to improve this. Interestingly, the firmware size increase on PYBV10 is about the same if LTO is enabled (although the firmwares are overall smaller.) |
2c3f941 to
6f3c7e0
Compare
What about: -00000016 T mp_obj_int_to_bytes_impl
+00000114 T mp_obj_int_to_bytesThat's 254 extra bytes. Is that similar on other ports that did decrease? |
I think so, this is the common function that the others now call. They grow by basically the same amount on both samd and rp2 ports. Here's samd: @@ -6342 +6338 @@
-00000016 T mp_obj_int_to_bytes_impl
+00000114 T mp_obj_int_to_bytesFull size diffs--- build-ADAFRUIT_ITSYBITSY_M4_EXPRESS-master/firmware.syms 2025-03-04 17:06:29.044966391 +1100
+++ build-ADAFRUIT_ITSYBITSY_M4_EXPRESS/firmware.syms 2025-03-04 17:07:24.983870265 +1100
@@ -993 +992,0 @@
-t $d
@@ -4381,3 +4379,0 @@
-t $t
-t $t
-t $t
@@ -4404,0 +4401 @@
+00000018 t allowed_args.0
@@ -4620 +4617 @@
-00000260 t compile_scope_inline_asm
+00000264 t compile_scope_inline_asm
@@ -4822 +4819 @@
-00000968 t emit_inline_thumb_op
+00000970 t emit_inline_thumb_op
@@ -5312 +5309 @@
-000000d8 t int_to_bytes
+0000005c t int_to_bytes
@@ -5758,4 +5755,3 @@
-0000002a T mp_binary_set_int
-0000016c T mp_binary_set_val
-0000008c T mp_binary_set_val_array
-000000f2 T mp_binary_set_val_array_from_int
+0000005a T mp_binary_set_int
+00000150 T mp_binary_set_val
+0000006e T mp_binary_set_val_array
@@ -5777 +5773 @@
-0000008c t mp_builtin_compile
+00000088 t mp_builtin_compile
@@ -6300 +6296 @@
-0000003a T mp_obj_exception_attr
+00000038 T mp_obj_exception_attr
@@ -6342 +6338 @@
-00000016 T mp_obj_int_to_bytes_impl
+00000114 T mp_obj_int_to_bytes
@@ -6477 +6473 @@
-00000542 T mp_qstr_const_hashes
+00000536 T mp_qstr_const_hashes
@@ -6479 +6475 @@
-000002a1 T mp_qstr_const_lengths
+0000029b T mp_qstr_const_lengths
@@ -6481 +6477 @@
-00000a9c T mp_qstr_const_pool
+00000a84 T mp_qstr_const_pool
@@ -6802 +6798 @@
-000000d2 T mpz_as_bytes
+0000009e T mpz_as_bytes
@@ -7172 +7168 @@
-00000084 t set_aligned
+00000080 t set_aligned
@@ -7245 +7241 @@
-00000034 t slice_attr
+00000030 t slice_attr
@@ -7402 +7398 @@
-00000070 t task_attr
+0000007c t task_attr
@@ -7553 +7549 @@
-00000200 t uctypes_struct_attr_op
+00000204 t uctypes_struct_attr_op
@@ -7599 +7595 @@
-00000038 t usb_device_attr
+00000034 t usb_device_attr--- build-RPI_PICO-master/firmware.syms 2025-03-04 17:01:17.898379768 +1100
+++ build-RPI_PICO/firmware.syms 2025-03-04 17:01:28.760234428 +1100
@@ -31,4 +30,0 @@
-t $d
-t $d
-t $d
-t $d
@@ -244 +239,0 @@
-t $d
@@ -630,0 +626,2 @@
+t $d
+r $d
@@ -1202,0 +1200 @@
+t $d
@@ -1536,0 +1535 @@
+t $d
@@ -5462,3 +5460,0 @@
-t $t
-t $t
-t $t
@@ -5503,0 +5500 @@
+00000018 r allowed_args.0
@@ -5826 +5823 @@
-00000278 t compile_scope_inline_asm
+00000274 t compile_scope_inline_asm
@@ -6144 +6141 @@
-00000498 t emit_inline_thumb_op
+00000494 t emit_inline_thumb_op
@@ -6781 +6778 @@
-000000d4 t int_to_bytes
+00000060 t int_to_bytes
@@ -6786 +6783 @@
-00000044 t iobase_ioctl
+00000040 t iobase_ioctl
@@ -7352,4 +7349,3 @@
-0000002a T mp_binary_set_int
-00000154 T mp_binary_set_val
-00000084 T mp_binary_set_val_array
-00000084 T mp_binary_set_val_array_from_int
+00000056 T mp_binary_set_int
+0000012e T mp_binary_set_val
+00000064 T mp_binary_set_val_array
@@ -7370 +7366 @@
-00000064 t mp_builtin_compile
+00000068 t mp_builtin_compile
@@ -7885 +7881 @@
-0000005c T mp_native_type_from_qstr
+00000064 T mp_native_type_from_qstr
@@ -7962 +7958 @@
-00000016 T mp_obj_int_to_bytes_impl
+00000110 T mp_obj_int_to_bytes
@@ -8109 +8105 @@
-000002ee R mp_qstr_const_hashes
+000002f1 R mp_qstr_const_hashes
@@ -8111 +8107 @@
-000002ee R mp_qstr_const_lengths
+000002f1 R mp_qstr_const_lengths
@@ -8113 +8109 @@
-00000bd0 R mp_qstr_const_pool
+00000bdc R mp_qstr_const_pool
@@ -8355 +8351 @@
-00000064 T mp_vfs_blockdev_init
+00000060 T mp_vfs_blockdev_init
@@ -8365 +8361 @@
-00000068 T mp_vfs_getcwd
+0000006c T mp_vfs_getcwd
@@ -8367,2 +8363,2 @@
-00000060 T mp_vfs_ilistdir
-00000070 t mp_vfs_ilistdir_it_iternext
+00000064 T mp_vfs_ilistdir
+00000074 t mp_vfs_ilistdir_it_iternext
@@ -8370 +8366 @@
-0000007c T mp_vfs_import_stat
+0000007a T mp_vfs_import_stat
@@ -8433 +8429 @@
-0000002e T mp_vfs_rename
+00000030 T mp_vfs_rename
@@ -8435 +8431 @@
-00000016 T mp_vfs_rmdir
+00000018 T mp_vfs_rmdir
@@ -8437 +8433 @@
-00000040 T mp_vfs_stat
+0000003c T mp_vfs_stat
@@ -8439 +8435 @@
-00000054 T mp_vfs_statvfs
+00000058 T mp_vfs_statvfs
@@ -8441 +8437 @@
-0000009c T mp_vfs_umount
+000000a0 T mp_vfs_umount
@@ -8450 +8446 @@
-00000140 T mpz_as_bytes
+000000ea T mpz_as_bytes
@@ -9025 +9021 @@
-00000084 t set_aligned
+0000007c t set_aligned
@@ -9102 +9098 @@
-00000038 t slice_attr
+00000034 t slice_attrAh! But the very big difference on these ports is that, unlike stm32, the That's the function which is still called from stm32/adc.c . So if we can find another way to implement that, the code would go down on stm32 (and we also wouldn't need the changes in #16858). |
|
EDIT: Accidentally buried the useful info in the |
Specifically pyb.ADC.read_timed and read_timed_multi are documented as supporting either an array of 8/16-bit integers or a bytearray.
|
Support signed param: result = int.from_bytes(bytearray(), order='big'|'little', signed=False|True) Add `length`, `byteorder`, `signed` according to the micropython#16311. Signed-off-by: Ihor Nehrutsa <Ihor.Nehrutsa@gmail.com>
Hmm. This exception is the most visible "breaking change" for MicroPython code which worked before this PR, so maybe we can add an explanatory error message similar to CPython (as this one is a bit obscure). |
I don't follow. In current master we get: So this never worked. Or do you mean that |
That's the one. |
This seems like it's only really a problem on Debug builds, but I think can't hurt to increase it on all windows builds. This work was funded through GitHub Sponsors. Signed-off-by: Angus Gratton <angus@redyak.com.au>
This removes the last usage of mp_binary_set_val_array_from_int(). It will be a little slower, but shouldn't be measurably so compared to the ADC sampling. This work was funded through GitHub Sponsors. Signed-off-by: Angus Gratton <angus@redyak.com.au>
If >1 unittest-enabled module is included, the results of the merged module won't match (as it runs some previously registered tests again). Signed-off-by: Angus Gratton <angus@redyak.com.au>
4342ec3 to
3a5beb4
Compare
| mp_binary_set_int(GET_SCALAR_SIZE(val_type & 7), self->flags == LAYOUT_BIG_ENDIAN, | ||
| self->addr + offset, val); | ||
| size_t item_size = GET_SCALAR_SIZE(val_type & 7); | ||
| mp_binary_set_int(item_size, self->addr + offset, item_size, val, self->flags == LAYOUT_BIG_ENDIAN); |
There was a problem hiding this comment.
As I understand it, this is a breaking change in behaviour because it can now raise if val doesn't fit.
Is it worth retaining existing behaviour simply by masking val as appropriate so that it always fits? (Then the uctypes_array_load_store.py test does not need to change.)
There was a problem hiding this comment.
Not quite, mp_binary_set_int() doesn't ever raise - the API just changed a little so it could be called from more places.
The changes in uctypes_array_load_store.py are to account for bytearray setters gaining overflow checks in MicroPython V2.0 (because moductypes creates bytearrays internally).
There was a problem hiding this comment.
Not quite,
mp_binary_set_int()doesn't ever raise - the API just changed a little so it could be called from more places.
Ah, I see.
The changes in
uctypes_array_load_store.pyare to account for bytearray setters gaining overflow checks in MicroPython V2.0 (because moductypes creates bytearrays internally).
Hmm, I don't see the original version of that test storing into a bytearray? It creates a bytearray only as a backing buffer for the uctypes struct.
There was a problem hiding this comment.
Hmm, yeah... I would classify my confidence about moductypes internals as "very low" but my understanding is that the relevant code path is here:
https://github.com/micropython/micropython/blob/master/extmod/moductypes.c#L502
Can see it by doing something like this:
>>> b = bytearray(5)
>>> desc = {"arr": (uctypes.ARRAY | 0, uctypes.UINT8 | 5)}
>>> s = uctypes.struct(uctypes.addressof(b), desc, uctypes.LITTLE_ENDIAN)
>>> type(s.arr)
<class 'bytearray'>
>>> desc = {"arr": (uctypes.ARRAY | 0, uctypes.INT8 | 5)}
>>> s = uctypes.struct(uctypes.addressof(b), desc, uctypes.LITTLE_ENDIAN)
>>> type(s.arr)
<class 'struct'>
Which is why the is_v2 differences in the test are all gated behind if is_v2 and item_sz == 1. (I'm not actually sure why the test passes here for INT8 when expecting the OverflowError, maybe all of the test values are in-range for INT8 but not UINT8...?)
This is still a weird change I admit, because other kinds of moductypes arrays won't add overflow checks in V2 but UINT8 will...
So can think of two possible ways forward:
- Change the test to explicitly check if
isinstance(s.arr, bytearray)which will make the reason for the special case in the test more obvious. - Remove the moductypes optimisation of treating
UINT8specially as a bytearray, so you always get back astructobject of some kind. I'm not sure what the other trade-offs of this might be.
Any preference?
There was a problem hiding this comment.
Interesting. In your snip above the second one (INT8) is not a bytearray, so that's why the check is not done.
If you modify extmod/uctypes_array_load_store.py to add print(s.arr) just before the inner for loop, you'll see that everything except UINT8 are internal uctypes struct's, but the UINT8 case is a bytearray.
Hmm...
There was a problem hiding this comment.
2. Remove the moductypes optimisation of treating
UINT8specially as a bytearray, so you always get back astructobject of some kind. I'm not sure what the other trade-offs of this might be.
That would make things cleaner/simpler, and not give any regressions wrt overflow checks.
But, it would probably give regressions in other areas, eg uctypes.struct can't be sliced, iterated, etc. Whereas bytearray can be.
Could we return a memoryview instead of bytearray? Or does memoryview also do overflow checking with this PR?
- Change the test to explicitly check if
isinstance(s.arr, bytearray)which will make the reason for the special case in the test more obvious.
I think at the very least do this option. Then it's clear what's going on.
There was a problem hiding this comment.
Interesting. In your snip above the second one (INT8) is not a bytearray, so that's why the check is not done.
Yes, sorry that's what I meant - the part I didn't get is why item_sz == 1 is a sufficient check given it matches both INT8 and UINT8 and they're different types.
I get it now, it's because the range of values used in each test is [-2, -1, 0, 1, 2]. So they're always in-range for all signed types, meaning that the full check if is_v2 and item_sz == 1 and (n < item_min or n > item_max): only hits the special-case for UINT8.
So, I can definitely rewrite the test to make the behaviour clearer. But that still depends on keeping the behaviour that an array of UINT8 is a bytearray type not the same struct type as the other arrays.
There was a problem hiding this comment.
To reword the options I can think of:
- Change the test to explicitly check if
isinstance(s.arr, bytearray)which will make the reason for the special case in the test more obvious. MicroPython V2 will have a breaking change for moductypes code that uses arrays ofUINT8and relies on implicit truncation, same as any other code which uses bytearray and relies on implicit truncation. - Remove the V2 bounds check for bytearray setters (making it different to the
arraymodule). Don't recommend this for the reasons given in the PR description (possibility for silent bugs, difference to CPython). - Remove the moductypes optimisation of having a UINT8 array be a bytearray, so the
arrelement always gives back a struct object of some kind. This will keep all moductypes behaviour the same in MicroPython V2. I'm not sure what the other trade-offs of not returning abytearrayin this case would be.
There was a problem hiding this comment.
Oops, our comments overlapped. I'll do option 1 for now, we can always rethink the V2 behaviour change later.
Could we return a memoryview instead of bytearray? Or does memoryview also do overflow checking with this PR?
With V2 it will, yes. Same code path.
There was a problem hiding this comment.
Have pushed a new test which makes the reason for the behaviour more explicit.
|
Following up on octoprobe failures: I ran the current version of this PR on ADAFRUIT_ITSYBITSY_M0_EXPRESS and there are 3 new failures introduced by this PR. FAIL
|
Thanks @dpgeorge. So if I understand correctly these are all running out of memory after the test module starts executing but before any test case runs? |
Yes, I think that's the case. Although I'm not 100% certain because it's hard to tell with the native emitter due to limited traceback info. I'll see if I can dig deeper to see exactly which allocation is failing. |
3a5beb4 to
9a8d0ba
Compare
I looked deeper into the above 3 failing tests. They all fail trying to I added a The message after
There are some small optimisations we can make to |
9a8d0ba to
fa992c4
Compare
In this case, how about: try:
import unittest
except MemoryError:
print("SKIP")
raise SystemExit... at least, for now. It's more boilerplate than I prefer, and there's a potential gap for a test which can import |
I tried that (although used BUT, running just Maybe we can live with that for now (so, include that try-import-unittest-except logic), and I can make a few optimisations to |
fa992c4 to
f6d4441
Compare
|
@dpgeorge Thanks for working through these. I've updated the tests according to the discussion. |
|
I made a PR to optimise |
This PR was originally a cherry-pick of CircuitPython commits 095c844, d103ac1, c592bd6 and 8664a65. However, substantial additional changes so the implementation has diverged a lot from CircuitPython's: - Keep CPython >= 3.11 defaults for int.to_bytes(), consistent with 80c5e76 and 0b432b3. - Refactors to reduce the code size impact of this change. Various code paths are now funneled into new function mp_obj_int_to_bytes() and existing function mp_binary_set_int(), except where a simple assignment is used for performance reasons (i.e. array, moductypes). - Keep the MicroPython behaviour of not overflow checking assignments to arrays, bytearrays, etc. - but enable overflow checks as part of MicroPython V2.0. - Update tests to work with the new behaviour (similar to CPython) and add coverage for new code and some corner cases. Some tests are converted to unittest so we can easily verify both current and V2.0 overflow. This work was funded through GitHub Sponsors. Signed-off-by: Angus Gratton <angus@redyak.com.au>
f6d4441 to
5ecf096
Compare
That's great! Do you want to keep the try/except guards in these tests, or pull them out? |
dpgeorge
left a comment
There was a problem hiding this comment.
Thanks for persisting with this PR, I know it's taken a while to get to this stage with a lot of back and forth.
It looks really good now. Nice to finally have int.to_bytes(signed) support.
Let's merge it!

Summary
This PR is attempting to bring some CircuitPython changes upstream to MicroPython. From these PRs:
The changes:
int.to_bytes(signed=bool)parameter. This removes the CPython incompatibility where currentlyint.to_bytesin MicroPython behaves as ifsigned=self < 0when converting.Note that this may require MicroPython code changes where it previously behaved differently to CPython:
int.to_bytes()was being called on a negative number will now raiseOverflowError. The fix is to addsigned=True.int.to_bytes()was silently truncating the output will now raiseOverflowError. The fix is either to ensure the length is sufficient for the value, or mask the value before converting it (i.e.(x & 0xFFFF).to_bytes(2).After cherry-picking commits directly from CircuitPython and resolving conflicts, some additional changes were needed (and in the end I've squashed most of them down into one):
byteorderandlength, as per py/objint: Make byteorder argument optional in byte-conversion methods. #14387 and CPython >=3.11.OverflowErrorfrom array constructors if a value is out of range, truncate the value instead. This is a documented difference between CPython and MicroPython, and after discussing with @dpgeorge decided changing it here has the potential to break existing MP code. The overflow checks are kept ifMICROPY_PREVIEW_VERSION_2is set, so that we can change behaviour to match CPython (and CircuitPython) in MicroPython 2. I guess Python code size may become a littler bigger if masking needs to be added, but it also eliminates a potential silent bug.mp_binary_set_val_array_from_int- after the above refactors this was no longer used except in stm32/adc.c and a coverage test. Removing it reduces the stm32 port code size substantially.Testing
Trade-offs and Alternatives