将Objective-C枚举常量转换为字符串名称



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

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

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

我一直希望苹果能提供一条出路——它终于来了吗?或者这是只有调试器才能做到的技巧——仅仅是运行时代码无法访问它?

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的调试信息——它(在这个编译单元中)是未使用的,所以它被删除了。

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

我最近研究了libclang,并决定通过解决您对从枚举值转换为字符串的繁琐工作的抱怨来实践它"(您必须手动写出所有内容/创建一个静态数组/将所有值放入字典并读回…等等)"。

这里有一个Python脚本,它将解析一个Cocoa文件,找到枚举(包括用NS_OPTIONSNS_ENUM定义的枚举),并吐出一个数组或一个函数(使用switch)来进行映射。数组选项利用了一种名为"指定(数组)初始值设定项"的C特性,通过该特性,数组成员可以显式地与特定索引配对:

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

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

匿名的enum(我认为至少在Foundation中都是单个成员)使用常量的名称直接转换为单个NSString。我已经把重点放在了负值常量的自动处理上,比如NSOrderedAscending——数组初始值设定项中的负索引不会编译,但函数/switch的替代方法会很好地工作;你只需要为这几个案例手动选择。

这个脚本已经在GitHub上发布了,它是完整的。麻省理工学院的执照,你喜欢怎么办就怎么办。我很乐意听到任何修改。

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

最新更新