Convert Objective-C enum constants to string names

2019-01-28 09:03发布

问题:

Previously, this was impossible (you have to write it all out by hand / create a static array / put all the values into a dictionary and read them back ... etc)

But I've noticed that the latest Xcode's lldb (4.6, maybe earlier versions too) is automatically converting enum-constants to strings.

My problem is that we use a lot of libraries - including Apple's own! - which use annoying public enums with no "value-to-string" method offered. So I end up having to (many, many times over) do the "well, since Mr. Library Author didn't do this, now I have to make the static array for them...".

I kept hoping Apple would provide a way out of this - is it finally here? Or is this some trick that only the debugger can do - mere runtime code has no access to it?

回答1:

lldb doesn't have any special capabilities regarding printing enum names. I think what you're seeing is the result of the enum values being recorded in the debug info (or not). For instance,

enum myenums {a = 0, b, c};
int main ()
{
 enum myenums var = b;
 return (int) var;  // break here
}

% xcrun clang -g a.c
% xcrun lldb a.out
(lldb) br s -p break
Breakpoint 1: where = a.out`main + 18 at a.c:5, address = 0x0000000100000f92
(lldb) r
[...]
-> 5     return (int) var;  // break here
   6    }
(lldb) p var
(myenums) $0 = b
(lldb) p (myenums) 0
(myenums) $1 = a
(lldb) 

If you look at the debug info for this binary (dwarfdump a.out.dSYM) you'll see that the variable var's type is myenums and the debug information includes the values of those enumerated types:

0x0000005a:     TAG_enumeration_type [5] *
                 AT_name( "myenums" )
                 AT_byte_size( 0x04 )
                 AT_decl_file( "/private/tmp/a.c" )
                 AT_decl_line( 1 )

0x00000062:         TAG_enumerator [6]  
                     AT_name( "a" )
                     AT_const_value( 0x0000000000000000 )

0x00000068:         TAG_enumerator [6]  
                     AT_name( "b" )
                     AT_const_value( 0x0000000000000001 )

0x0000006e:         TAG_enumerator [6]  
                     AT_name( "c" )
                     AT_const_value( 0x0000000000000002 )

If I add another enum to my sample file which isn't used anywhere,

enum myenums {a = 0, b, c};
enum otherenums {d = 0, e, f}; // unused in this CU
int main ()
{
 enum myenums var = b;
 return (int) var;  // break here
}

re-compile and look at the DWARF again via dwarfdump, I won't find any debug info describing otherenums - it is unused (in this compilation unit) and so it is elided.



回答2:

As has been noted, the constant names are not accessible after compilation except in the debugger. H2CO3's suggestion of the TO_STR() macro should get you through most uses, I imagine.

I was looking into libclang recently and decided to exercise it by addressing your complaint about the drudgery of translating from enum value to string "(you have to write it all out by hand / create a static array / put all the values into a dictionary and read them back ... etc)".

Here's a Python script that will parse a Cocoa file, find the enums (including those defined with NS_OPTIONS and NS_ENUM), and spit out either an array or a function (using a switch) to do the mapping. The array option takes advantage of a C feature called "designated (array) initializers", whereby array members can be explicitly paired with a particular index:

int arr[] = { [1] = 2, [3] = 8, [7] = 12 };

Note that this is not a sparse array -- any non-specified indexes before the last are still created, and initialized with 0. The implication is that enums intended to be used as bitmasks, whose values are 1 << 2, 1 << 3, 1 << 4, and so on, will create fairly large arrays for relatively few used values. A function is probably the better choice in those cases.

Anonymous enums (which I think are all single-member, in Foundation at least) are converted directly to a single NSString using the name of the constant. I've punted on the automatic handling of constants that are negative-valued, like NSOrderedAscending -- a negative index in the array initializer won't compile, but the function/switch alternative will work fine; you just have to choose that manually for those few cases.

The script is up on GitHub, and here it is in its entirety. MIT license, so do with it what you like. I'll be happy to hear about any modifications.

#!/usr/bin/env python

"""
CocoaEnumToString

Parse a specified Cocoa header file and emit ObjC code to translate the values
of any enums found within into their names as NSStrings.
"""
# Copyright 2013 Joshua Caswell.
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# 
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

import itertools
import argparse
import sys
import re
from os import path

from clang import cindex
from clang.cindex import CursorKind

def all_children(node):
    return itertools.chain(iter([node]), *map(all_children, 
                                              node.get_children()))

def all_constant_decls(enum):
    return iter(child for child in all_children(enum) if 
                    child.kind == CursorKind.ENUM_CONSTANT_DECL)

def indent_all_lines(s, indent):
    return '\n'.join(indent + line for line in s.split('\n'))

def format_anonymous(enum, title):
    const_str = 'NSString * const {} = @"{}";\n'
    constants = [const_str.format(title.replace('%e', constant.spelling),
                                  constant.spelling)
                            for constant in all_constant_decls(enum)]
    return "".join(constants)


def format_as_array(enum, title, indent):
    all_members = ['[{0}] = @"{0}"'.format(constant.spelling) 
                        for constant in all_constant_decls(enum)]
    all_members = ",\n".join(all_members)

    title = title.replace('%e', enum.spelling)
    array_str = "NSString * const {}[] = {{\n{}\n}};"
    return array_str.format(title, indent_all_lines(all_members, indent))

def format_as_func(enum, title, indent):
    case_str = 'case {0}:\n{1}return @"{0}";'
    all_cases = [case_str.format(constant.spelling, indent)
                        for constant in all_constant_decls(enum)]
    all_cases.append('default:\n{}@"";'.format(indent))
    all_cases = "\n".join(all_cases)

    switch = "switch( val ){{\n{}\n}}".format(indent_all_lines(all_cases, 
                                                               indent))
    title = title.replace('%e', enum.spelling)
    func_str = "NSString * {}({} val){{\n{}\n}}"
    return func_str.format(title, enum.spelling, 
                           indent_all_lines(switch, indent))


parser = argparse.ArgumentParser(description="Use libclang to find enums in "
                                  "the specified Objective-C file and emit a "
                                  "construct (array or function) that "
                                  "maps between the constant values and "
                                  "their names.")
# This argument must be added to the parser first for its default to override
# that of --arr and --fun
parser.add_argument("-c", "--construct", default="array",
                    help="Specify 'function' or any prefix ('f', 'fun', etc.) "
                         "to emit a function that uses a switch statement for "
                         "the mapping; specify 'array' or any prefix for "
                         "an array (this is the default). Whichever of -c, "
                         "--arr, or --fun occurs last in the argument list "
                         "will dictate the output.")
parser.add_argument("--arr", "--array", action="store_const", const="array",
                    dest="construct", help="Emit an array for the mapping.")
parser.add_argument("-e", "--enums", action="append",
                    help="Specify particular enums to capture; by default "
                    "all enums in the given file are used. This argument may "
                    "be present multiple times. Names which are not found in "
                    "the input file are ignored.")
parser.add_argument("--fun", "--func", "--function", action="store_const", 
                    const="function", dest="construct", 
                    help="Emit a function for the mapping.")
parser.add_argument("-i", "--indent", default="4s",
                    help="Number and type of character to use for indentation."
                    " Digits plus either 't' (for tabs) or 's' (for spaces), "
                    "e.g., '4s', which is the default.")
parser.add_argument("-n", "--name", default="StringFor%e",
                    help="Name for the construct; the prefix will "
# Escape percent sign because argparse is doing some formatting of its own.
                    "be added. Any appearances of '%%e' in this argument will "
                    "be replaced with each enum name. The default is "
                    "'StringFor%%e'.")
parser.add_argument("-o", "--output",
                    help="If this argument is present, output should go to a "
                    "file which will be created at the specified path. An "
                    "error will be raised if the file already exists.")
parser.add_argument("-p", "--prefix", default="",
                    help="Cocoa-style prefix to add to the name of emitted "
                    "construct, e.g. 'NS'")
parser.add_argument("file", help="Path to the file which should be parsed.")


arguments = parser.parse_args()

if "array".startswith(arguments.construct):
    format_enum = format_as_array 
elif "function".startswith(arguments.construct):
    format_enum = format_as_func
else:
   parser.error("Neither 'function' nor 'array' specified for construct.")

match = re.match(r"(\d*)([st])", arguments.indent)
if not match.group(2):
    parser.error("Neither tabs nor spaces specified for indentation.")
else:
    indent_char = '\t' if match.group(2) == 't' else ' '
    indent = indent_char * int(match.group(1) or 1)

if arguments.output:
    if path.exists(arguments.output):
        sys.stderr.write("Error: Requested output file exists: "
                         "{}\n".format(arguments.output))
        sys.exit(1)
    else:
        out_f = open(arguments.output, 'w')
else:
    out_f = sys.stdout

target_file_name = arguments.file


# Ignore the fact that system libclang probably doesn't match the version
# of the Python bindings.
cindex.Config.set_compatibility_check(False)
# Use what's likely to be a newer version than that found in /usr/lib
cindex.Config.set_library_file("/Applications/Xcode.app/Contents/Developer/"
                               "Toolchains/XcodeDefault.xctoolchain/usr/lib/"
                               "libclang.dylib")

# Preprocessor macros that resolve into enums; these are defined in
# NSObjCRuntime.h, but including that directly causes redefinition errors due
# to it being also imported.
ns_options_def = ("NS_OPTIONS(_type, _name)=enum _name : "
                 "_type _name; enum _name : _type")
ns_enum_def = ("NS_ENUM(_type, _name)=enum _name : _type _name; "
              "enum _name : _type")

tu = cindex.TranslationUnit.from_source(target_file_name, 
                                        args=["-ObjC", "-D", ns_enum_def,
                                              "-D", ns_options_def, "-D",
                                              "NS_ENUM_AVAILABLE="])


enums = [node for node in all_children(tu.cursor) if 
                node.kind == CursorKind.ENUM_DECL and
                node.location.file.name.find(target_file_name) != -1]
if arguments.enums:
    enums = filter(lambda enum: enum.spelling in arguments.enums, enums)

title = arguments.prefix + arguments.name

for enum in enums:
    if not enum.spelling:
        out_f.write(format_anonymous(enum, title))
    else:
        out_f.write(format_enum(enum, title, indent))
    out_f.write("\n\n")