925bf060c188d84bbd96c114dd3d502ba3d776df
[nit.git] / contrib / objcwrapper / src / objc_generator.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 # Code generation
16 module objc_generator
17
18 import opts
19
20 import objc_model
21
22 redef class Sys
23
24 # Path to the output file
25 var opt_output = new OptionString("Output file", "-o")
26
27 # Shall `init` methods/constructors be wrapped as methods?
28 #
29 # By default, these methods/constructors are wrapped as extern constructors.
30 # So initializing an extern Objective-C object looks like:
31 # ~~~nitish
32 # var o = new NSArray.init_with_array(some_other_array)
33 # ~~~
34 #
35 # If this option is set, the object must first be allocated and then initialized.
36 # This is closer to the Objective-C behavior:
37 # ~~~nitish
38 # var o = new NSArray
39 # o.init_with_array(some_other_array)
40 # ~~~
41 var opt_init_as_methods = new OptionBool(
42 "Wrap `init...` constructors as Nit methods instead of Nit constructors",
43 "--init-as-methods")
44
45 private var objc_to_nit_types: Map[String, String] is lazy do
46 var types = new HashMap[String, String]
47 types["char"] = "Byte"
48 types["short"] = "Int"
49 types["short int"] = "Int"
50 types["int"] = "Int"
51 types["long"] = "Int"
52 types["long int"] = "Int"
53 types["long long"] = "Int"
54 types["long long int"] = "Int"
55 types["float"] = "Float"
56 types["double"] = "Float"
57 types["long double"] = "Float"
58
59 types["NSUInteger"] = "Int"
60 types["NSInteger"] = "Int"
61 types["CGFloat"] = "Float"
62 types["BOOL"] = "Bool"
63
64 types["id"] = "NSObject"
65 types["constid"] = "NSObject"
66 types["SEL"] = "NSObject"
67 types["void"] = "Pointer"
68
69 return types
70 end
71 end
72
73 redef class ObjcModel
74 redef fun knows_type(objc_type) do return super or
75 objc_to_nit_types.keys.has(objc_type)
76 end
77
78 # Wrapper generator
79 class CodeGenerator
80
81 # `ObjcModel` to wrap
82 var model: ObjcModel
83
84 # Generate Nit code to wrap `classes`
85 fun generate
86 do
87 var classes = model.classes
88
89 # Open specified path or stdin
90 var file
91 var path = opt_output.value
92 if path != null then
93 if path.file_extension != "nit" then
94 print_error "Warning: output file path does not end with '.nit'"
95 end
96
97 file = new FileWriter.open(path)
98 else
99 file = stdout
100 end
101
102 # Generate code
103 file.write """
104 # File generated by objcwrapper with the following command:
105 # {{{program_name}}} {{{args.join(" ")}}}
106
107 """
108
109 file.write "import cocoa::foundation\n"
110 for classe in classes do
111 write_class(classe, file)
112 end
113
114 if path != null then file.close
115 end
116
117 private fun write_class(classe: ObjcClass, file: Writer)
118 do
119 # Class header
120 file.write """
121
122 extern class {{{classe.name}}} in "ObjC" `{ {{{classe.name}}} * `}
123 """
124
125 # Supers
126 for super_name in classe.super_names do file.write """
127 super {{{super_name}}}
128 """
129 if classe.super_names.is_empty then file.write """
130 super NSObject
131 """
132
133 # Constructor or constructors
134 write_constructors(classe, file)
135
136 # Attributes
137 for attribute in classe.attributes do
138 write_attribute(attribute, file)
139 end
140
141 # Instance methods '-'
142 for method in classe.methods do
143 if not model.knows_all_types(method) then method.is_commented = true
144
145 if not opt_init_as_methods.value and method.is_init then continue
146 if method.is_class_property then continue
147
148 write_method_signature(method, file)
149 write_objc_method_call(method, file)
150 end
151
152 file.write """
153 end
154 """
155
156 # Class methods '+'
157 for method in classe.methods do
158 if not method.is_class_property then continue
159
160 write_method_signature(method, file)
161 write_objc_method_call(method, file)
162 end
163 end
164
165 private fun write_constructors(classe: ObjcClass, file: Writer)
166 do
167 if opt_init_as_methods.value then
168 # A single constructor for `alloc`
169 file.write """
170
171 new in "ObjC" `{
172 return [{{{classe.name}}} alloc];
173 `}
174 """
175 return
176 end
177
178 # A constructor per `init...` method
179 for method in classe.methods do
180 if not method.is_init then continue
181
182 if not model.knows_all_types(method) then method.is_commented = true
183
184 write_method_signature(method, file)
185
186 write_objc_init_call(classe.name, method, file)
187 end
188 end
189
190 private fun write_attribute(attribute: ObjcAttribute, file: Writer)
191 do
192 if not model.knows_type(attribute.return_type) then attribute.is_commented = true
193
194 write_attribute_getter(attribute, file)
195 # TODO write_attribute_setter if there is no `readonly` annotation
196 end
197
198 private fun write_attribute_getter(attribute: ObjcAttribute, file: Writer)
199 do
200 var nit_attr_name = attribute.name.to_snake_case
201 var nit_attr_type = attribute.return_type.objc_to_nit_type
202
203 var c = attribute.comment_str
204
205 file.write """
206
207 {{{attribute.doc}}}
208 {{{c}}} fun {{{nit_attr_name}}}: {{{nit_attr_type}}} in "ObjC" `{
209 {{{c}}} return [self {{{attribute.name}}}];
210 {{{c}}} `}
211 """
212 end
213
214 private fun write_attribute_setter(attribute: ObjcAttribute, file: Writer)
215 do
216 var nit_attr_name = attribute.name.to_snake_case
217 var nit_attr_type = attribute.return_type.objc_to_nit_type
218
219 var c = attribute.comment_str
220
221 file.write """
222
223 {{{attribute.doc}}}
224 {{{c}}} fun {{{nit_attr_name}}}=(value: {{{nit_attr_type}}}) in "ObjC" `{
225 {{{c}}} return self.{{{attribute.name}}} = value;
226 {{{c}}} `}
227 """
228 end
229
230 private fun write_method_signature(method: ObjcMethod, file: Writer)
231 do
232 var c = method.comment_str
233
234 # Build Nit method name
235 var name = ""
236 for param in method.params do
237 name += param.name[0].to_upper.to_s + param.name.substring_from(1)
238 end
239 name = name.to_snake_case
240
241 if name == "init" then name = ""
242
243 # If class method, prefix with class name
244 if method.is_class_property then name = "{method.objc_class.name.to_snake_case}_{name}"
245
246 # Kind of method
247 var fun_keyword = "fun"
248 if not opt_init_as_methods.value and method.is_init then
249 fun_keyword = "new"
250 end
251
252 # Params
253 var params = new Array[String]
254 for param in method.params do
255 if param.is_single then break
256 params.add "{param.variable_name}: {param.return_type.objc_to_nit_type}"
257 end
258
259 var params_with_par = ""
260 if params.not_empty then params_with_par = "({params.join(", ")})"
261
262 # Return
263 var ret = ""
264 if method.return_type != "void" and fun_keyword != "new" then
265 ret = ": {method.return_type.objc_to_nit_type}"
266 end
267
268 file.write """
269
270 {{{method.doc}}}
271 {{{c}}}{{{fun_keyword}}} {{{name}}}{{{params_with_par}}}{{{ret}}} in "ObjC" `{
272 """
273 end
274
275 # Write a combined call to alloc and to a constructor/method
276 private fun write_objc_init_call(class_name: String, method: ObjcMethod, file: Writer)
277 do
278 # Method name and other params
279 var params = new Array[String]
280 for param in method.params do
281 if not param.is_single then
282 params.add "{param.name}: {param.variable_name}"
283 else params.add param.name
284 end
285
286 var c = method.comment_str
287
288 file.write """
289 {{{c}}} return [[{{{class_name}}} alloc] {{{params.join(" ")}}}];
290 {{{c}}}`}
291 """
292 end
293
294 private fun write_objc_method_call(method: ObjcMethod, file: Writer)
295 do
296 # Is there a value to return?
297 var ret = ""
298 if method.return_type != "void" then ret = "return "
299
300 # Method name and other params
301 var params = new Array[String]
302 for param in method.params do
303 if not param.is_single then
304 params.add "{param.name}: {param.variable_name}"
305 else params.add param.name
306 end
307
308 # Receiver, instance or class
309 var recv = "self"
310 if method.is_class_property then recv = method.objc_class.name
311
312 var c = method.comment_str
313
314 file.write """
315 {{{c}}} {{{ret}}}[{{{recv}}} {{{params.join(" ")}}}];
316 {{{c}}}`}
317 """
318 end
319 end
320
321 redef class Text
322 # Nit equivalent to this type
323 private fun objc_to_nit_type: String
324 do
325 var types = sys.objc_to_nit_types
326
327 if types.has_key(self) then
328 return types[self]
329 else
330 return to_s
331 end
332 end
333 end
334
335 redef class ObjcProperty
336 private fun comment_str: String do if is_commented then
337 return "#"
338 else return ""
339
340 # Full documentation to be generated for the Nit code
341 private fun doc: String is abstract
342 end
343
344 redef class ObjcMethod
345 private fun indent: String do return if is_class_property then "" else "\t"
346
347 redef fun comment_str do return indent + super
348
349 redef fun doc
350 do
351 var recv = if is_class_property then objc_class.name else "self"
352 return "{indent}# Wraps: `[{recv} {params.join(" ")}]`"
353 end
354 end
355
356 redef class ObjcAttribute
357 redef fun doc do return "\t# Wraps: `{objc_class.name}.{name}`"
358 end