【问题标题】:Convert Objective-C enum constants to string names将 Objective-C 枚举常量转换为字符串名称
【发布时间】:2013-08-07 22:10:17
【问题描述】:

以前,这是不可能的(您必须手动将其全部写出来/创建一个静态数组/将所有值放入字典并读回......等等)

但我注意到最新的 Xcode 的 lldb(4.6,也可能是更早的版本)会自动将枚举常量转换为字符串。

我的问题是我们使用了很多库——包括 Apple 自己的! - 使用恼人的公共枚举,没有提供“值到字符串”的方法。所以我最终不得不(很多很多次)做“好吧,既然图书馆作者先生没有这样做,现在我必须为他们制作静态数组......”。

我一直希望 Apple 能提供一种解决方法——它终于来了吗?或者这是只有调试器才能做到的一些技巧 - 仅仅运行时代码无法访问它?

【问题讨论】:

  • 是的,只有调试器才能做到——编译器会自动生成调试符号并从枚举中生成字符串。一种替代方法是 #define TO_STR(m) #m 预处理器宏,它可能会或可能不会满足您的用例。

标签: objective-c xcode llvm lldb


【解决方案1】:

lldb 没有任何关于打印枚举名称的特殊功能。我认为您所看到的是枚举值记录在调试信息中(或不记录)的结果。例如,

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) 

如果您查看此二进制文件 (dwarfdump a.out.dSYM) 的调试信息,您会看到变量 var 的类型是 myenums,并且调试信息包括这些枚举类型的值:

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 )

如果我在示例文件中添加另一个未在任何地方使用的枚举,

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
}

重新编译并通过 dwarfdump 再次查看 DWARF,我找不到任何描述 otherenums 的调试信息 - 它未使用(在此编译单元中),因此被省略了。

【讨论】:

    【解决方案2】:

    如前所述,除了在调试器中之外,编译后无法访问常量名称。我想,H2CO3 对TO_STR() 宏的建议应该可以帮助您完成大多数用途。

    我最近正在研究 libclang,并决定通过解决您对从枚举值转换为字符串的苦差事的抱怨来锻炼它“(您必须手动将其全部写出来/创建一个静态数组/将所有值放入一本字典,然后把它们读回来……等等)”。

    这是一个 Python 脚本,它将解析一个 Cocoa 文件,找到枚举(包括用NS_OPTIONS 和NS_ENUM 定义的枚举),然后输出一个数组或一个函数(使用switch)来执行映射。数组选项利用了一个名为 "designated (array) initializers" 的 C 功能,数组成员可以显式地与特定索引配对:

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

    请注意,这不是一个稀疏数组——最后一个索引之前的任何未指定索引仍然被创建,并用 0 初始化。这意味着枚举旨在用作位掩码,其值是1 << 2、1 << 3、1 << 4 等,将为相对较少的使用值创建相当大的数组。在这些情况下,函数可能是更好的选择。

    匿名enums(我认为至少在Foundation中都是单个成员)使用常量名称直接转换为单个NSString。我已经强调了负值常量的自动处理,例如NSOrderedAscending——数组初始值设定项中的负索引不会编译,但函数/switch 替代方法可以正常工作;您只需要为这几种情况手动选择即可。

    脚本是up on GitHub,这里是完整的。 MIT 许可证,随心所欲。我很乐意听到任何修改。

    #!/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")
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-04-12
      • 2015-06-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多