Commits

Armin Rigo committed 7092769

Moving the determination of the base integer type of an enum
out of C code, into the 'cffi' package. This is work in progress;
it should eventually permit the new test to fully pass. For now
wrote a warning in the doc.

Comments (0)

Files changed (6)

c/_cffi_backend.c

     char *ename;
     PyObject *enumerators, *enumvalues;
     PyObject *dict1 = NULL, *dict2 = NULL, *combined = NULL, *tmpkey = NULL;
-    ffi_type *ffitype;
     int name_size;
-    CTypeDescrObject *td;
+    CTypeDescrObject *td, *basetd;
     Py_ssize_t i, n;
-    struct aligncheck_int { char x; int y; };
-    struct aligncheck_long { char x; long y; };
-    long smallest_item = 0;
-    unsigned long largest_item = 0;
-    int size, flags;
-
-    if (!PyArg_ParseTuple(args, "sO!O!:new_enum_type",
+
+    if (!PyArg_ParseTuple(args, "sO!O!O!:new_enum_type",
                           &ename,
                           &PyTuple_Type, &enumerators,
-                          &PyTuple_Type, &enumvalues))
+                          &PyTuple_Type, &enumvalues,
+                          &CTypeDescr_Type, &basetd))
         return NULL;
 
     n = PyTuple_GET_SIZE(enumerators);
         return NULL;
     }
 
+    if (!(basetd->ct_flags & (CT_PRIMITIVE_SIGNED|CT_PRIMITIVE_UNSIGNED))) {
+        PyErr_SetString(PyExc_TypeError,
+                        "expected a primitive signed or unsigned base type");
+        return NULL;
+    }
+
     dict1 = PyDict_New();
     if (dict1 == NULL)
         goto error;
         goto error;
 
     for (i=n; --i >= 0; ) {
-        long lvalue;
-        unsigned long ulvalue;
+        long long lvalue;
         PyObject *value = PyTuple_GET_ITEM(enumvalues, i);
         tmpkey = PyTuple_GET_ITEM(enumerators, i);
         Py_INCREF(tmpkey);
                 goto error;
             }
         }
-        lvalue = PyLong_AsLong(value);
-        if (PyErr_Occurred()) {
-            PyErr_Clear();
-            ulvalue = PyLong_AsUnsignedLong(value);
-            if (PyErr_Occurred()) {
-                PyErr_Format(PyExc_OverflowError,
-                             "enum '%s' declaration for '%s' does not fit "
-                             "a long or unsigned long",
-                             ename, PyText_AS_UTF8(tmpkey));
-                goto error;
-            }
-            if (ulvalue > largest_item)
-                largest_item = ulvalue;
-        }
-        else {
-            if (lvalue < 0) {
-                if (lvalue < smallest_item)
-                    smallest_item = lvalue;
-            }
-            else {
-                ulvalue = (unsigned long)lvalue;
-                if (ulvalue > largest_item)
-                    largest_item = ulvalue;
-            }
-        }
+        if (convert_from_object((char*)&lvalue, basetd, value) < 0)
+            goto error;     /* out-of-range or badly typed 'value' */
         if (PyDict_SetItem(dict1, tmpkey, value) < 0)
             goto error;
         if (PyDict_SetItem(dict2, value, tmpkey) < 0)
         tmpkey = NULL;
     }
 
-    if (smallest_item < 0) {
-        flags = CT_PRIMITIVE_SIGNED | CT_PRIMITIVE_FITS_LONG | CT_IS_ENUM;
-        if (smallest_item == (int)smallest_item &&
-                 largest_item <= (unsigned long)INT_MAX) {
-            size = sizeof(int);
-        }
-        else if (largest_item <= (unsigned long)LONG_MAX) {
-            size = sizeof(long);
-        }
-        else {
-            PyErr_Format(PyExc_OverflowError,
-                         "enum '%s' values don't all fit into either 'long' "
-                         "or 'unsigned long'", ename);
-            goto error;
-        }
-    }
-    else if (sizeof(unsigned int) < sizeof(unsigned long) &&
-             largest_item == (unsigned int)largest_item) {
-        flags = CT_PRIMITIVE_UNSIGNED | CT_PRIMITIVE_FITS_LONG | CT_IS_ENUM;
-        size = sizeof(unsigned int);
-    }
-    else {
-        flags = CT_PRIMITIVE_UNSIGNED | CT_IS_ENUM;
-        size = sizeof(unsigned long);
-    }
-
     combined = PyTuple_Pack(2, dict1, dict2);
     if (combined == NULL)
         goto error;
     Py_CLEAR(dict2);
     Py_CLEAR(dict1);
 
-    switch (size) {
-    case 4: ffitype = &ffi_type_sint32; break;
-    case 8: ffitype = &ffi_type_sint64; break;
-    default: Py_FatalError("'int' or 'long' is not 4 or 8 bytes"); return NULL;
-    }
-
     name_size = strlen("enum ") + strlen(ename) + 1;
     td = ctypedescr_new(name_size);
     if (td == NULL)
     memcpy(td->ct_name, "enum ", strlen("enum "));
     memcpy(td->ct_name + strlen("enum "), ename, name_size - strlen("enum "));
     td->ct_stuff = combined;
-    td->ct_size = size;
-    td->ct_length = size == sizeof(int) ? offsetof(struct aligncheck_int, y)
-                                        : offsetof(struct aligncheck_long, y);
-    td->ct_extra = ffitype;
-    td->ct_flags = flags;
+    td->ct_size = basetd->ct_size;
+    td->ct_length = basetd->ct_length;   /* alignment */
+    td->ct_extra = basetd->ct_extra;     /* ffi type  */
+    td->ct_flags = basetd->ct_flags | CT_IS_ENUM;
     td->ct_name_position = name_size - 1;
     return (PyObject *)td;
 
     py.test.raises(TypeError, callback, BFunc, cb, -42)
 
 def test_enum_type():
-    BEnum = new_enum_type("foo", (), ())
+    BUInt = new_primitive_type("unsigned int")
+    BEnum = new_enum_type("foo", (), (), BUInt)
     assert repr(BEnum) == "<ctype 'enum foo'>"
     assert BEnum.kind == "enum"
     assert BEnum.cname == "enum foo"
     assert BEnum.elements == {}
     #
-    BEnum = new_enum_type("foo", ('def', 'c', 'ab'), (0, 1, -20))
+    BInt = new_primitive_type("int")
+    BEnum = new_enum_type("foo", ('def', 'c', 'ab'), (0, 1, -20), BInt)
     assert BEnum.kind == "enum"
     assert BEnum.elements == {-20: 'ab', 0: 'def', 1: 'c'}
     # 'elements' is not the real dict, but merely a copy
     BEnum.elements[2] = '??'
     assert BEnum.elements == {-20: 'ab', 0: 'def', 1: 'c'}
     #
-    BEnum = new_enum_type("bar", ('ab', 'cd'), (5, 5))
+    BEnum = new_enum_type("bar", ('ab', 'cd'), (5, 5), BUInt)
     assert BEnum.elements == {5: 'ab'}
     assert BEnum.relements == {'ab': 5, 'cd': 5}
 
 def test_cast_to_enum():
-    BEnum = new_enum_type("foo", ('def', 'c', 'ab'), (0, 1, -20))
+    BInt = new_primitive_type("int")
+    BEnum = new_enum_type("foo", ('def', 'c', 'ab'), (0, 1, -20), BInt)
+    assert sizeof(BEnum) == sizeof(BInt)
     e = cast(BEnum, 0)
     assert repr(e) == "<cdata 'enum foo' 0: def>"
     assert repr(cast(BEnum, -42)) == "<cdata 'enum foo' -42>"
     assert int(cast(BEnum, -242 + 2**128)) == -242
     assert string(cast(BEnum, -242 + 2**128)) == '-242'
     #
-    BEnum = new_enum_type("bar", ('def', 'c', 'ab'), (0, 1, 20))
+    BUInt = new_primitive_type("unsigned int")
+    BEnum = new_enum_type("bar", ('def', 'c', 'ab'), (0, 1, 20), BUInt)
     e = cast(BEnum, -1)
     assert repr(e) == "<cdata 'enum bar' 4294967295>"     # unsigned int
+    #
+    BLong = new_primitive_type("long")
+    BEnum = new_enum_type("baz", (), (), BLong)
+    assert sizeof(BEnum) == sizeof(BLong)
+    e = cast(BEnum, -1)
+    assert repr(e) == "<cdata 'enum baz' -1>"
 
 def test_enum_with_non_injective_mapping():
-    BEnum = new_enum_type("foo", ('ab', 'cd'), (7, 7))
+    BInt = new_primitive_type("int")
+    BEnum = new_enum_type("foo", ('ab', 'cd'), (7, 7), BInt)
     e = cast(BEnum, 7)
     assert repr(e) == "<cdata 'enum foo' 7: ab>"
     assert string(e) == 'ab'
 
 def test_enum_in_struct():
-    BEnum = new_enum_type("foo", ('def', 'c', 'ab'), (0, 1, -20))
+    BInt = new_primitive_type("int")
+    BEnum = new_enum_type("foo", ('def', 'c', 'ab'), (0, 1, -20), BInt)
     BStruct = new_struct_type("bar")
     BStructPtr = new_pointer_type(BStruct)
     complete_struct_or_union(BStruct, [('a1', BEnum, -1)])
         "unsupported operand type for int(): 'NoneType'" in str(e.value)) #PyPy
     py.test.raises(TypeError, 'p.a1 = "def"')
     if sys.version_info < (3,):
-        BEnum2 = new_enum_type(unicode("foo"), (unicode('abc'),), (5,))
+        BEnum2 = new_enum_type(unicode("foo"), (unicode('abc'),), (5,), BInt)
         assert string(cast(BEnum2, 5)) == 'abc'
         assert type(string(cast(BEnum2, 5))) is str
 
     max_int = max_uint // 2
     max_ulong = 2 ** (size_of_long()*8) - 1
     max_long = max_ulong // 2
-    # 'unsigned int' case
-    e = new_enum_type("foo", ('a', 'b'), (0, 3))
-    assert sizeof(e) == size_of_int()
-    assert int(cast(e, -1)) == max_uint     # 'e' is unsigned
-    e = new_enum_type("foo", ('a', 'b'), (0, max_uint))
-    assert sizeof(e) == size_of_int()
-    assert int(cast(e, -1)) == max_uint
-    assert e.elements == {0: 'a', max_uint: 'b'}
-    assert e.relements == {'a': 0, 'b': max_uint}
-    # 'signed int' case
-    e = new_enum_type("foo", ('a', 'b'), (-1, max_int))
-    assert sizeof(e) == size_of_int()
-    assert int(cast(e, -1)) == -1
-    assert e.elements == {-1: 'a', max_int: 'b'}
-    assert e.relements == {'a': -1, 'b': max_int}
-    e = new_enum_type("foo", ('a', 'b'), (-max_int-1, max_int))
-    assert sizeof(e) == size_of_int()
-    assert int(cast(e, -1)) == -1
-    assert e.elements == {-max_int-1: 'a', max_int: 'b'}
-    assert e.relements == {'a': -max_int-1, 'b': max_int}
-    # 'unsigned long' case
-    e = new_enum_type("foo", ('a', 'b'), (0, max_long))
-    assert sizeof(e) == size_of_long()
-    assert int(cast(e, -1)) == max_ulong     # 'e' is unsigned
-    e = new_enum_type("foo", ('a', 'b'), (0, max_ulong))
-    assert sizeof(e) == size_of_long()
-    assert int(cast(e, -1)) == max_ulong
-    assert e.elements == {0: 'a', max_ulong: 'b'}
-    assert e.relements == {'a': 0, 'b': max_ulong}
-    # 'signed long' case
-    e = new_enum_type("foo", ('a', 'b'), (-1, max_long))
-    assert sizeof(e) == size_of_long()
-    assert int(cast(e, -1)) == -1
-    assert e.elements == {-1: 'a', max_long: 'b'}
-    assert e.relements == {'a': -1, 'b': max_long}
-    e = new_enum_type("foo", ('a', 'b'), (-max_long-1, max_long))
-    assert sizeof(e) == size_of_long()
-    assert int(cast(e, -1)) == -1
-    assert e.elements == {-max_long-1: 'a', max_long: 'b'}
-    assert e.relements == {'a': -max_long-1, 'b': max_long}
-    # overflow: both negative items and items larger than max_long
-    e = py.test.raises(OverflowError, new_enum_type, "foo", ('a', 'b'),
-                       (-1, max_long + 1))
-    assert str(e.value) == (
-        "enum 'foo' values don't all fit into either 'long' "
-        "or 'unsigned long'")
-    # overflow: items smaller than -max_long-1
-    e = py.test.raises(OverflowError, new_enum_type, "foo", ('a', 'b'),
-                       (-max_long-2, 5))
-    assert str(e.value) == (
-        "enum 'foo' declaration for 'a' does not fit a long or unsigned long")
-    # overflow: items larger than max_ulong
-    e = py.test.raises(OverflowError, new_enum_type, "foo", ('a', 'b'),
-                       (5, max_ulong+1))
-    assert str(e.value) == (
-        "enum 'foo' declaration for 'b' does not fit a long or unsigned long")
+    for BPrimitive in [new_primitive_type("int"),
+                       new_primitive_type("unsigned int"),
+                       new_primitive_type("long"),
+                       new_primitive_type("unsigned long")]:
+        for x in [max_uint, max_int, max_ulong, max_long]:
+            for testcase in [x, x+1, -x-1, -x-2]:
+                if int(cast(BPrimitive, testcase)) == testcase:
+                    # fits
+                    BEnum = new_enum_type("foo", ("AA",), (testcase,),
+                                          BPrimitive)
+                    assert int(cast(BEnum, testcase)) == testcase
+                else:
+                    # overflows
+                    py.test.raises(OverflowError, new_enum_type,
+                                   "foo", ("AA",), (testcase,), BPrimitive)
 
 def test_callback_returning_enum():
     BInt = new_primitive_type("int")
-    BEnum = new_enum_type("foo", ('def', 'c', 'ab'), (0, 1, -20))
+    BEnum = new_enum_type("foo", ('def', 'c', 'ab'), (0, 1, -20), BInt)
     def cb(n):
         if n & 1:
             return cast(BEnum, n)
 
 def test_callback_returning_enum_unsigned():
     BInt = new_primitive_type("int")
-    BEnum = new_enum_type("foo", ('def', 'c', 'ab'), (0, 1, 20))
+    BUInt = new_primitive_type("unsigned int")
+    BEnum = new_enum_type("foo", ('def', 'c', 'ab'), (0, 1, 20), BUInt)
     def cb(n):
         if n & 1:
             return cast(BEnum, n)

cffi/backend_ctypes.py

         CTypesFunctionPtr._fix_class()
         return CTypesFunctionPtr
 
-    def new_enum_type(self, name, enumerators, enumvalues):
+    def new_enum_type(self, name, enumerators, enumvalues, CTypesInt):
         assert isinstance(name, str)
         reverse_mapping = dict(zip(reversed(enumvalues),
                                    reversed(enumerators)))
-        smallest = min(enumvalues or [0])
-        largest = max(enumvalues or [0])
-        if smallest < 0:
-            if largest == ctypes.c_int(largest).value:
-                tp = 'int'
-            elif largest == ctypes.c_long(largest).value:
-                tp = 'long'
-            else:
-                raise OverflowError
-        else:
-            if largest == ctypes.c_uint(largest).value:
-                tp = 'unsigned int'
-            elif largest == ctypes.c_ulong(largest).value:
-                tp = 'unsigned long'
-            else:
-                raise OverflowError
-        CTypesInt = self.ffi._get_cached_btype(model.PrimitiveType(tp))
         #
         class CTypesEnum(CTypesInt):
             __slots__ = []
     partial = False
     partial_resolved = False
 
-    def __init__(self, name, enumerators, enumvalues):
+    def __init__(self, name, enumerators, enumvalues, baseinttype=None):
         self.name = name
         self.enumerators = enumerators
         self.enumvalues = enumvalues
+        self.baseinttype = baseinttype
 
     def check_not_partial(self):
         if self.partial and not self.partial_resolved:
 
     def build_backend_type(self, ffi, finishlist):
         self.check_not_partial()
+        base_btype = self.build_baseinttype(ffi, finishlist)
         return global_cache(self, ffi, 'new_enum_type', self.name,
-                            self.enumerators, self.enumvalues, key=self)
+                            self.enumerators, self.enumvalues,
+                            base_btype, key=self)
 
+    def build_baseinttype(self, ffi, finishlist):
+        if self.baseinttype is not None:
+            return self.baseinttype.get_cached_btype(ffi, finishlist)
+        #
+        if self.enumvalues:
+            smallest_value = min(self.enumvalues)
+            largest_value = max(self.enumvalues)
+        else:
+            smallest_value = 0
+            largest_value = 0
+        if smallest_value < 0:   # needs a signed type
+            sign = 1
+            candidate1 = PrimitiveType("int")
+            candidate2 = PrimitiveType("long")
+        else:
+            sign = 0
+            candidate1 = PrimitiveType("unsigned int")
+            candidate2 = PrimitiveType("unsigned long")
+        btype1 = candidate1.get_cached_btype(ffi, finishlist)
+        btype2 = candidate2.get_cached_btype(ffi, finishlist)
+        size1 = ffi.sizeof(btype1)
+        size2 = ffi.sizeof(btype2)
+        if (smallest_value >= ((-1) << (8*size1-1)) and
+            largest_value < (1 << (8*size1-sign))):
+            return btype1
+        if (smallest_value >= ((-1) << (8*size2-1)) and
+            largest_value < (1 << (8*size2-sign))):
+            return btype2
+        raise api.CDefError("%s values don't all fit into either 'long' "
+                            "or 'unsigned long'" % self._get_c_name(''))
 
 def unknown_type(name, structname=None):
     if structname is None:

doc/source/index.rst

    Enum types follow the GCC rules: they are defined as the first of
    ``unsigned int``, ``int``, ``unsigned long`` or ``long`` that fits
    all numeric values.  Note that the first choice is unsigned.  In CFFI
-   0.5 and before, it was always ``int``.
+   0.5 and before, it was always ``int``.  *Unimplemented: if the very
+   large values are not declared, the enum size will be incorrectly
+   deduced!  Work around this by making sure that you name the largest
+   value and/or any negative value in the cdef.*
 
 
 Debugging dlopen'ed C libraries

testing/test_verify.py

     res = lib2.myfunc(lib2.AA)
     assert res == 2
 
+def test_enum_size():
+    cases = [('123',           4, 4294967295),
+             ('4294967295U',   4, 4294967295),
+             ('-123',          4, -1),
+             ('-2147483647-1', 4, -1),
+             ]
+    if FFI().sizeof("long") == 8:
+        cases += [('4294967296L',        8, 2**64-1),
+                  ('%dUL' % (2**64-1),   8, 2**64-1),
+                  ('-2147483649L',       8, -1),
+                  ('%dL-1L' % (1-2**63), 8, -1)]
+    for hidden_value, expected_size, expected_minus1 in cases:
+        ffi = FFI()
+        ffi.cdef("enum foo_e { AA, BB, ... };")
+        lib = ffi.verify("enum foo_e { AA, BB=%s };" % hidden_value)
+        assert lib.AA == 0
+        assert lib.BB == eval(hidden_value.replace('U', '').replace('L', ''))
+        assert ffi.sizeof("enum foo_e") == expected_size
+        assert int(ffi.cast("enum foo_e", -1)) == expected_minus1
+    # test with the large value hidden:
+    # disabled so far, doesn't work
+##    for hidden_value, expected_size, expected_minus1 in cases:
+##        ffi = FFI()
+##        ffi.cdef("enum foo_e { AA, BB, ... };")
+##        lib = ffi.verify("enum foo_e { AA, BB=%s };" % hidden_value)
+##        assert lib.AA == 0
+##        assert ffi.sizeof("enum foo_e") == expected_size
+##        assert int(ffi.cast("enum foo_e", -1)) == expected_minus1
+
 def test_string_to_voidp_arg():
     ffi = FFI()
     ffi.cdef("int myfunc(void *);")
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.