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