Skip to content
This repository has been archived by the owner on Mar 26, 2020. It is now read-only.

Commit

Permalink
Safe ObjC constants [breaking change]
Browse files Browse the repository at this point in the history
Fixes #171.  Djinni constants which are not primitives, enums, or strings must
now be accessed in Objective-C class members, rather than constant
variables.  This avoids unsafe ordering of initialization of constant objects.
  • Loading branch information
Xianwen Chen authored and Andrew Twyman committed Apr 22, 2016
1 parent 54df6f1 commit e9c23ac
Show file tree
Hide file tree
Showing 12 changed files with 311 additions and 188 deletions.
117 changes: 117 additions & 0 deletions src/source/BaseObjcGenerator.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/**
* Copyright 2016 Dropbox, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package djinni

import djinni.ast.Record.DerivingType
import djinni.ast._
import djinni.generatorTools._
import djinni.meta._
import djinni.syntax.Error
import djinni.writer.IndentWriter

import scala.collection.mutable
import scala.collection.parallel.immutable

abstract class BaseObjcGenerator(spec: Spec) extends Generator(spec) {

val marshal = new ObjcMarshal(spec)

object ObjcConstantType extends Enumeration {
val ConstVariable, ConstMethod = Value
}

def writeObjcConstVariableDecl(w: IndentWriter, c: Const, s: String): Unit = {
val nullability = marshal.nullability(c.ty.resolved).fold("")(" __" + _)
val td = marshal.fqFieldType(c.ty) + nullability
// MBinary | MList | MSet | MMap are not allowed for constants.
w.w(s"${td} const $s${idObjc.const(c.ident)}")
}

/**
* Gererate the definition of Objc constants.
*/
def generateObjcConstants(w: IndentWriter, consts: Seq[Const], selfName: String,
genType: ObjcConstantType.Value) = {
def boxedPrimitive(ty: TypeRef): String = {
val (_, needRef) = marshal.toObjcType(ty)
if (needRef) "@" else ""
}

def writeObjcConstValue(w: IndentWriter, ty: TypeRef, v: Any): Unit = v match {
case l: Long => w.w(boxedPrimitive(ty) + l.toString)
case d: Double if marshal.fieldType(ty) == "float" => w.w(boxedPrimitive(ty) + d.toString + "f")
case d: Double => w.w(boxedPrimitive(ty) + d.toString)
case b: Boolean => w.w(boxedPrimitive(ty) + (if (b) "YES" else "NO"))
case s: String => w.w("@" + s)
case e: EnumValue => w.w(idObjc.enum(e.ty + "_" + e.name))
case v: ConstRef => w.w(selfName + idObjc.const (v.name))
case z: Map[_, _] => { // Value is record
val recordMdef = ty.resolved.base.asInstanceOf[MDef]
val record = recordMdef.body.asInstanceOf[Record]
val vMap = z.asInstanceOf[Map[String, Any]]
val head = record.fields.head
w.w(s"[[${marshal.typename(ty)} alloc] initWith${IdentStyle.camelUpper(head.ident)}:")
writeObjcConstValue(w, head.ty, vMap.apply(head.ident))
w.nestedN(2) {
val skipFirst = SkipFirst()
for (f <- record.fields) skipFirst {
w.wl
w.w(s"${idObjc.field(f.ident)}:")
writeObjcConstValue(w, f.ty, vMap.apply(f.ident))
}
}
w.w("]")
}
}

def writeObjcConstMethImpl(c: Const, w: IndentWriter) {
val label = "+"
val nullability = marshal.nullability(c.ty.resolved).fold("")(" __" + _)
val ret = marshal.fqFieldType(c.ty) + nullability
val decl = s"$label ($ret)${idObjc.method(c.ident)}"
writeAlignedObjcCall(w, decl, List(), "", p => ("",""))
w.wl

w.braced {
var static_var = s"s_${idObjc.method(c.ident)}"
w.w(s"static ${marshal.fqFieldType(c.ty)} const ${static_var} = ")
writeObjcConstValue(w, c.ty, c.value)
w.wl(";")
w.wl(s"return $static_var;")
}
}

genType match {
case ObjcConstantType.ConstVariable => {
for (c <- consts if marshal.canBeConstVariable(c)) {
w.wl
writeObjcConstVariableDecl(w, c, selfName)
w.w(s" = ")
writeObjcConstValue(w, c.ty, c.value)
w.wl(";")
}
}
case ObjcConstantType.ConstMethod => {
for (c <- consts if !marshal.canBeConstVariable(c)) {
writeObjcConstMethImpl(c, w)
w.wl
}
}
}
}
}

144 changes: 35 additions & 109 deletions src/source/ObjcGenerator.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,7 @@ import djinni.writer.IndentWriter
import scala.collection.mutable
import scala.collection.parallel.immutable

class ObjcGenerator(spec: Spec) extends Generator(spec) {

val marshal = new ObjcMarshal(spec)
class ObjcGenerator(spec: Spec) extends BaseObjcGenerator(spec) {

class ObjcRefs() {
var body = mutable.TreeSet[String]()
Expand Down Expand Up @@ -65,58 +63,17 @@ class ObjcGenerator(spec: Spec) extends Generator(spec) {

def bodyName(ident: String): String = idObjc.ty(ident) + "." + spec.objcppExt // Must be a Obj-C++ file in case the constants are not compile-time constant expressions

def writeObjcConstVariable(w: IndentWriter, c: Const, s: String): Unit = {
def writeObjcConstMethDecl(c: Const, w: IndentWriter) {
val label = "+"
val nullability = marshal.nullability(c.ty.resolved).fold("")(" __" + _)
val td = marshal.fqFieldType(c.ty) + nullability
// MBinary | MList | MSet | MMap are not allowed for constants.
w.w(s"${td} const $s${idObjc.const(c.ident)}")
}

def generateObjcConstants(w: IndentWriter, consts: Seq[Const], selfName: String) = {
def boxedPrimitive(ty: TypeRef): String = {
val (_, needRef) = toObjcType(ty)
if (needRef) "@" else ""
}
def writeObjcConstValue(w: IndentWriter, ty: TypeRef, v: Any): Unit = v match {
case l: Long => w.w(boxedPrimitive(ty) + l.toString)
case d: Double if marshal.fieldType(ty) == "float" => w.w(boxedPrimitive(ty) + d.toString + "f")
case d: Double => w.w(boxedPrimitive(ty) + d.toString)
case b: Boolean => w.w(boxedPrimitive(ty) + (if (b) "YES" else "NO"))
case s: String => w.w("@" + s)
case e: EnumValue => w.w(idObjc.enum(e.ty + "_" + e.name))
case v: ConstRef => w.w(selfName + idObjc.const (v.name))
case z: Map[_, _] => { // Value is record
val recordMdef = ty.resolved.base.asInstanceOf[MDef]
val record = recordMdef.body.asInstanceOf[Record]
val vMap = z.asInstanceOf[Map[String, Any]]
val head = record.fields.head
w.w(s"[[${marshal.typename(ty)} alloc] initWith${IdentStyle.camelUpper(head.ident)}:")
writeObjcConstValue(w, head.ty, vMap.apply(head.ident))
w.nestedN(2) {
val skipFirst = SkipFirst()
for (f <- record.fields) skipFirst {
w.wl
w.w(s"${idObjc.field(f.ident)}:")
writeObjcConstValue(w, f.ty, vMap.apply(f.ident))
}
}
w.w("]")
}
}

w.wl("#pragma clang diagnostic push")
w.wl("#pragma clang diagnostic ignored " + q("-Wunused-variable"))
for (c <- consts) {
w.wl
writeObjcConstVariable(w, c, selfName)
w.w(s" = ")
writeObjcConstValue(w, c.ty, c.value)
w.wl(";")
}
w.wl
w.wl("#pragma clang diagnostic pop")
val ret = marshal.fqFieldType(c.ty) + nullability
val decl = s"$label ($ret)${idObjc.method(c.ident)}"
writeAlignedObjcCall(w, decl, List(), ";", p => ("",""))
}

/**
* Generate Interface
*/
override def generateInterface(origin: String, ident: Ident, doc: Doc, typeParams: Seq[TypeParam], i: Interface) {
val refs = new ObjcRefs()
i.methods.map(m => {
Expand All @@ -138,11 +95,12 @@ class ObjcGenerator(spec: Spec) extends Generator(spec) {
writeAlignedObjcCall(w, decl, method.params, "", p => (idObjc.field(p.ident), s"(${marshal.paramType(p.ty)})${idObjc.local(p.ident)}"))
}

// Generate the header file for Interface
writeObjcFile(marshal.headerName(ident), origin, refs.header, w => {
for (c <- i.consts) {
for (c <- i.consts if marshal.canBeConstVariable(c)) {
writeDoc(w, c.doc)
w.w(s"extern ")
writeObjcConstVariable(w, c, self)
writeObjcConstVariableDecl(w, c, self)
w.wl(s";")
}
w.wl
Expand All @@ -154,15 +112,23 @@ class ObjcGenerator(spec: Spec) extends Generator(spec) {
writeObjcFuncDecl(m, w)
w.wl(";")
}
for (c <- i.consts if !marshal.canBeConstVariable(c)) {
w.wl
writeDoc(w, c.doc)
writeObjcConstMethDecl(c, w)
}
w.wl
w.wl("@end")
})

// Generate the implementation file for Interface
if (i.consts.nonEmpty) {
refs.body.add("#import " + q(spec.objcIncludePrefix + marshal.headerName(ident)))
writeObjcFile(bodyName(ident.name), origin, refs.body, w => {
generateObjcConstants(w, i.consts, self)
generateObjcConstants(w, i.consts, self, ObjcConstantType.ConstVariable)
})
// For constants implemented via Methods, we generate their definitions in the
// corresponding ObjcCpp file (i.e.: `ClassName`+Private.mm)
}
}

Expand Down Expand Up @@ -196,6 +162,7 @@ class ObjcGenerator(spec: Spec) extends Generator(spec) {

val firstInitializerArg = if(r.fields.isEmpty) "" else IdentStyle.camelUpper("with_" + r.fields.head.ident.name)

// Generate the header file for record
writeObjcFile(marshal.headerName(objcName), origin, refs.header, w => {
writeDoc(w, doc)
w.wl(s"@interface $self : NSObject")
Expand All @@ -209,6 +176,12 @@ class ObjcGenerator(spec: Spec) extends Generator(spec) {
writeInitializer("-", "init")
if (!r.ext.objc) writeInitializer("+", IdentStyle.camelLower(objcName))

for (c <- r.consts if !marshal.canBeConstVariable(c)) {
w.wl
writeDoc(w, c.doc)
writeObjcConstMethDecl(c, w)
}

for (f <- r.fields) {
w.wl
writeDoc(w, f.doc)
Expand All @@ -224,17 +197,19 @@ class ObjcGenerator(spec: Spec) extends Generator(spec) {
// Constants come last in case one of them is of the record's type
if (r.consts.nonEmpty) {
w.wl
for (c <- r.consts) {
for (c <- r.consts if marshal.canBeConstVariable(c)) {
writeDoc(w, c.doc)
w.w(s"extern ")
writeObjcConstVariable(w, c, noBaseSelf);
writeObjcConstVariableDecl(w, c, noBaseSelf);
w.wl(s";")
}
}
})

// Generate the implementation file for record
writeObjcFile(bodyName(objcName), origin, refs.body, w => {
if (r.consts.nonEmpty) generateObjcConstants(w, r.consts, noBaseSelf)
if (r.consts.nonEmpty) generateObjcConstants(w, r.consts, noBaseSelf, ObjcConstantType.ConstVariable)

w.wl
w.wl(s"@implementation $self")
w.wl
Expand Down Expand Up @@ -268,6 +243,8 @@ class ObjcGenerator(spec: Spec) extends Generator(spec) {
w.wl
}

if (r.consts.nonEmpty) generateObjcConstants(w, r.consts, noBaseSelf, ObjcConstantType.ConstMethod)

if (r.derivingTypes.contains(DerivingType.Eq)) {
w.wl("- (BOOL)isEqual:(id)other")
w.braced {
Expand Down Expand Up @@ -439,55 +416,4 @@ class ObjcGenerator(spec: Spec) extends Generator(spec) {
f(w)
})
}

// TODO: this should be in ObjcMarshal
// Return value: (Type_Name, Is_Class_Or_Not)
def toObjcType(ty: TypeRef): (String, Boolean) = toObjcType(ty.resolved, false)
def toObjcType(ty: TypeRef, needRef: Boolean): (String, Boolean) = toObjcType(ty.resolved, needRef)
def toObjcType(tm: MExpr): (String, Boolean) = toObjcType(tm, false)
def toObjcType(tm: MExpr, needRef: Boolean): (String, Boolean) = {
def f(tm: MExpr, needRef: Boolean): (String, Boolean) = {
tm.base match {
case MOptional =>
// We use "nil" for the empty optional.
assert(tm.args.size == 1)
val arg = tm.args.head
arg.base match {
case MOptional => throw new AssertionError("nested optional?")
case m => f(arg, true)
}
case o =>
val base = o match {
case p: MPrimitive => if (needRef) (p.objcBoxed, true) else (p.objcName, false)
case MString => ("NSString", true)
case MDate => ("NSDate", true)
case MBinary => ("NSData", true)
case MOptional => throw new AssertionError("optional should have been special cased")
case MList => ("NSArray", true)
case MSet => ("NSSet", true)
case MMap => ("NSDictionary", true)
case d: MDef => d.defType match {
case DEnum => if (needRef) ("NSNumber", true) else (idObjc.ty(d.name), false)
case DRecord => (idObjc.ty(d.name), true)
case DInterface =>
val ext = d.body.asInstanceOf[Interface].ext
if (ext.cpp) (s"${idObjc.ty(d.name)}*", false) else (s"id<${idObjc.ty(d.name)}>", false)
}
case e: MExtern => if(needRef) (e.objc.boxed, true) else (e.objc.typename, e.objc.pointer)
case p: MParam => throw new AssertionError("Parameter should not happen at Obj-C top level")
}
base
}
}
f(tm, needRef)
}

// TODO: this should be in ObjcMarshal
def toObjcTypeDef(ty: TypeRef): String = toObjcTypeDef(ty.resolved, false)
def toObjcTypeDef(ty: TypeRef, needRef: Boolean): String = toObjcTypeDef(ty.resolved, needRef)
def toObjcTypeDef(tm: MExpr): String = toObjcTypeDef(tm, false)
def toObjcTypeDef(tm: MExpr, needRef: Boolean): String = {
val (name, asterisk) = toObjcType(tm, needRef)
name + (if (asterisk) " *" else " ")
}
}
18 changes: 18 additions & 0 deletions src/source/ObjcMarshal.scala
Original file line number Diff line number Diff line change
Expand Up @@ -146,4 +146,22 @@ class ObjcMarshal(spec: Spec) extends Marshal(spec) {
name + (if(needRef) " *" else "")
}

/**
* This method returns whether we can use global variable to represent a given constant.
*
* We can use global variables for constants which are safe to create during static init, which are numbers
* strings, and optional strings. Anything else needs to be a class method.
*/
def canBeConstVariable(c:Const): Boolean = c.ty.resolved.base match {
case MPrimitive(_,_,_,_,_,_,_,_) => true
case MString => true
case MOptional =>
assert(c.ty.resolved.args.size == 1)
val arg = c.ty.resolved.args.head
arg.base match {
case MString => true
case _ => false
}
case _ => false
}
}
7 changes: 6 additions & 1 deletion src/source/ObjcppGenerator.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import djinni.writer.IndentWriter

import scala.collection.mutable

class ObjcppGenerator(spec: Spec) extends Generator(spec) {
class ObjcppGenerator(spec: Spec) extends BaseObjcGenerator(spec) {

val objcMarshal = new ObjcMarshal(spec)
val objcppMarshal = new ObjcppMarshal(spec)
Expand Down Expand Up @@ -186,6 +186,11 @@ class ObjcppGenerator(spec: Spec) extends Generator(spec) {
}
}
}

if (i.consts.nonEmpty) {
w.wl
generateObjcConstants(w, i.consts, self, ObjcConstantType.ConstMethod)
}
}

if (i.ext.objc) {
Expand Down
Loading

0 comments on commit e9c23ac

Please sign in to comment.