Commit f149dc12fcc4a5ab3a6f6715736ed3139cdfcc10

Authored by Andreas Rumpf
1 parent 24d7cba1

first somewhat working version of SqueakNim

README.md
1 1 # NimSqueak - Squeak and Nim interop via FFI
2 2  
3   -Nim can produce dynamically loaded libraries that follow the C conventions. Squeak/Pharo has over the years developed several FFI mechanisms, and we can also make VM plugins, but for now this code uses the old "true and tested" Squeak FFI that Terf uses today for all its OpenGL calls.
  3 +Nim can produce dynamically loaded libraries that follow the C conventions.
  4 +Squeak/Pharo has over the years developed several FFI mechanisms, and we can
  5 +also make VM plugins, but for now this code uses the old "true and tested"
  6 +Squeak FFI that Terf uses today for all its OpenGL calls.
4 7  
5   -I also wrote an article when I experimented: http://goran.krampe.se/2014/11/03/squeak-to-nim/
  8 +I also wrote an article when I experimented:
  9 +http://goran.krampe.se/2014/11/03/squeak-to-nim/
6 10  
7   -In the testlib directory we have the files from that article. See README.md there for a concrete rundown on how to generate the .st file.
  11 +In the testlib directory we have the files from that article. See README.md
  12 +there for a concrete rundown on how to generate the .st file.
8 13  
9 14 ## Squeak FFI
10 15  
11   -What follows is a text written by Sean DeNigris describing the FFI. You can also see the code in the client image of course, with tests and class comments etc. And then we have the actual SqueakFFIPrims plugin (VM level code) that you can find in the terf-client-vm repo.
  16 +What follows is a text written by Sean DeNigris describing the FFI. You can
  17 +also see the code in the client image of course, with tests and class comments
  18 +etc. And then we have the actual SqueakFFIPrims plugin (VM level code) that
  19 +you can find in the terf-client-vm repo.
12 20  
13 21 ## How does FFI work?
14 22  
... ... @@ -35,14 +43,21 @@ system: aString
35 43 ^self externalCallFailed.
36 44 ```
37 45  
38   -`system: aString` above is the Smalltalk signature. Each named parameter is separated with a keyword. Smalltalk messages are normally camelCase, so if a nim proc was called `drink(bottles: int, brand: string)` then we could typically turn that into `drinkBottles: bottles brand: brand`. The unique (per class) so called `selector` (=name) of such a method in Smalltalk would be `drinkBottles:brand:`. Thus Nim overloading would not work (but we don't need that here).
  46 +`system: aString` above is the Smalltalk signature. Each named parameter is
  47 +separated with a keyword. Smalltalk messages are normally camelCase, so if
  48 +a nim proc was called `drink(bottles: int, brand: string)` then we could
  49 +typically turn that into `drinkBottles: bottles brand: brand`. The unique
  50 +(per class) so called `selector` (=name) of such a method in Smalltalk would
  51 +be `drinkBottles:brand:`. Thus Nim overloading would not work (but we don't
  52 +need that here).
39 53  
40 54 Then comes the so called pragma that describes the C function to call:
41 55  
42 56 ```
43 57 <apicall: long 'system' (char*) module: 'libSystem.dylib'>
44 58 ```
45   -Function specification should be the first line in the method and enclosed in angle brackets: < > containing:
  59 +Function specification should be the first line in the method and enclosed in
  60 +angle brackets: < > containing:
46 61 1. Calling Convention, either apicall: (Pascal convention) or cdecl: (C convention)
47 62 - Mac - use either one
48 63 - Unix - use cdecl
... ... @@ -116,15 +131,19 @@ Module Location, where the external library file lives
116 131 - Change the VM's Info.plist "SqueakPluginsBuiltInOrLocalOnly" key from "true" to "false."
117 132 Caveats
118 133 - security
119   - - malicious users could call arbitrary functions in the OS e.g. "format c:" from "system.dll" [7]
  134 + - malicious users could call arbitrary functions in the OS e.g.
  135 + "format c:" from "system.dll" [7]
120 136 - VMs do not protect against buffer overflow from bad parameters [8]:
121 137 "this would require an attacker to execute arbitrary Smalltalk
122 138 code on your server. Of course if they can do that they own you
123   - anyway, especially if you allow FFi or use the OSProcess plugin" - John McIntosh
  139 + anyway, especially if you allow FFi or use the OSProcess plugin"
  140 + - John McIntosh
124 141  
125 142 * difficulty
126   - - if you make a mistake you'll not drop into the debugger but Squeak will just crash [2]
127   - - If you crash Squeak when it is running the garbage collector, then you know your FFI code is leaking bits into object memory [2]
  143 + - if you make a mistake you'll not drop into the debugger but Squeak
  144 + will just crash [2]
  145 + - If you crash Squeak when it is running the garbage collector, then
  146 + you know your FFI code is leaking bits into object memory [2]
128 147  
129 148 What do I need to use FFI with Squeak?
130 149  
... ... @@ -138,8 +157,7 @@ References:
138 157 [2] http://wiki.squeak.org/squeak/2424
139 158 [3] http://wiki.squeak.org/squeak/5716
140 159 [4] http://wiki.squeak.org/squeak/2426
141   -[5]
142   -http://forum.world.st/squeak-dev-Alien-Squeak-FFI-issues-on-Snow-Leopard-td85608.html
  160 +[5] http://forum.world.st/squeak-dev-Alien-Squeak-FFI-issues-on-Snow-Leopard-td85608.html
143 161 [6] http://wiki.squeak.org/squeak/5846
144 162 [7] http://forum.world.st/FFI-Callbacks-td54056.html#a54073
145 163 [8] http://forum.world.st/Security-td99624.html#a99635:
... ...
src/squeaknim.nim 0 → 100644
  1 +
  2 +import macros, strutils
  3 +
  4 +const
  5 + pragmaPos = 4
  6 + paramPos = 3
  7 + intType = when sizeof(int) == 8: "longlong" else: "long"
  8 + uintType = when sizeof(int) == 8: "ulonglong" else: "ulong"
  9 +
  10 +var
  11 + dllName {.compileTime.}: string = "SqueakNimTest"
  12 + stCode {.compileTime.}: string = ""
  13 +
  14 +template setModulename*(s: string) =
  15 + ## Sets the DLL name. This is also used to set the 'category' in the generated
  16 + ## classes.
  17 + static:
  18 + dllName = s
  19 +
  20 +template writeExternalLibrary*() =
  21 + static:
  22 + addf(stCode, """ExternalLibrary subclass: #$1
  23 + instanceVariableNames: ''
  24 + classVariableNames: ''
  25 + poolDictionaries: ''
  26 + category: '$1'!
  27 +
  28 +!$1 class methodsFor: 'primitives' stamp: 'SqueakNim'!
  29 +""", capitalize(dllName))
  30 +
  31 +template writeSmallTalkCode*(filename: string) =
  32 + ## You need to invoke this template to write the produced SmallTalk code to
  33 + ## a file.
  34 + static:
  35 + writeFile(filename, stCode)
  36 +
  37 +proc mapTypeToC(symbolicType: NimNode): string {.compileTime.} =
  38 + let t = symbolicType.getType
  39 + if symbolicType.kind == nnkSym and t.typeKind == ntyObject:
  40 + return $symbolicType
  41 + case t.typeKind
  42 + of ntyTuple:
  43 + result = $t
  44 + when false:
  45 + let tt = if t.kind == nnkBracketExpr: t else: t.getType
  46 + expectKind t, nnkBracketExpr
  47 + result = "struct {"
  48 + for i in 0 .. < tt.len:
  49 + result.addf "$# Field$#;\n", mapTypeToC(tt[i]), $i
  50 + result.add "}"
  51 + of ntyArray, ntyArrayConstr:
  52 + # implement this!
  53 + result = "XXX"
  54 + of ntyOpenArray:
  55 + result = mapTypeToC(t[1]) & "* " & intType
  56 + of ntyPtr, ntyVar:
  57 + expectKind t, nnkBracketExpr
  58 + result = mapTypeToC(t[1]) & "*"
  59 + of ntyCString: result = "char*"
  60 + of ntyInt: result = intType
  61 + of ntyInt8: result = "sbyte"
  62 + of ntyInt16: result = "short"
  63 + of ntyInt32: result = "long"
  64 + of ntyInt64: result = "longlong"
  65 + of ntyUInt: result = uintType
  66 + of ntyUInt8: result = "ubyte"
  67 + of ntyUInt16: result = "ushort"
  68 + of ntyUInt32: result = "ulong"
  69 + of ntyUInt64: result = "ulonglong"
  70 + of ntyFloat, ntyFloat64: result = "double"
  71 + of ntyFloat32: result = "float"
  72 + of ntyBool, ntyChar, ntyEnum: result = "char"
  73 + else: quit "Error: cannot wrap to Squeak " & treeRepr(t)
  74 +
  75 +macro exportSt*(className: string; body: stmt): stmt =
  76 + # generates something like:
  77 +
  78 + # system: aString
  79 + #"Some kind of comment"
  80 + #
  81 + # <apicall: long 'system' (char*) module: 'libSystem.dylib'>
  82 + # ^self externalCallFailed.
  83 + result = body
  84 + result[pragmaPos].add(ident"exportc", ident"dynlib", ident"cdecl")
  85 + let params = result[paramPos]
  86 + let procName = $result[0]
  87 + var st = procName
  88 + #echo treeRepr params
  89 + if params.len > 1:
  90 + expectKind params[1], nnkIdentDefs
  91 + let ident = $params[1][0]
  92 + if ident.len > 1:
  93 + st.add(ident.capitalize & ": " & ident)
  94 + else:
  95 + st.add(": " & ident)
  96 + # return type:
  97 + var apicall = "<cdecl: " & mapTypeToC(params[0]) & " '" &
  98 + procName & "' ("
  99 + var counter = 0
  100 + # parameter types:
  101 + for i in 1.. <params.len:
  102 + let param = params[i]
  103 + let L = param.len
  104 + for j in 0 .. param.len-3:
  105 + let name = param[j]
  106 + let typ = param[L-2]
  107 + if counter > 0:
  108 + apicall.add(" ")
  109 + st.addf(" $1: $1", name)
  110 + apicall.add(mapTypeToC(typ))
  111 + inc counter
  112 + apicall.add(") module: '" & dllName & "'>\n" &
  113 + " ^self externalCallFailed.\n")
  114 + stCode.add(st & "\n\"Generated by NimSqueak\"\n" & apicall)
  115 +
  116 +macro wrapObject*(name: string; typ: stmt): stmt =
  117 + ## Declares a SmallTalk wrapper class.
  118 + let name = name.strVal.capitalize
  119 + var t = typ.getType()
  120 + if t.typeKind == ntyTypeDesc:
  121 + expectKind t, nnkBracketExpr
  122 + t = t[1]
  123 +
  124 + if t.kind != nnkObjectTy: t = t.getType
  125 + expectKind t, nnkObjectTy
  126 + t = t[1]
  127 + expectKind t, nnkRecList
  128 + var fields = ""
  129 + for i in 0.. < t.len:
  130 + expectKind t[i], nnkSym
  131 + fields.addf "($# '$#')\n", $t[i], mapTypeToC(t[i])
  132 +
  133 + let st = """ExternalStructure subclass: #$1
  134 + instanceVariableNames: ''
  135 + classVariableNames: ''
  136 + poolDictionaries: 'FFIConstants'
  137 + category: '$2'!
  138 +
  139 +$1 class
  140 + instanceVariableNames: ''!
  141 +
  142 + !$1 class methodsFor: 'field definition' stamp: 'SqueakNim'!
  143 + fields
  144 + ^#(
  145 + $3
  146 + )! !
  147 +
  148 + $1 compileFields!
  149 + $1 defineFields.
  150 +
  151 +""" % [name, dllName, fields]
  152 + stCode.add(st)
  153 + result = newStmtList()
  154 +
... ...
testlib/README.md
1   -The file to generate given testlib.nim would be `Testlib.st`. This is the class old "chunk format" in which the content is separated by `!` marks, in a rather oddish way, but anyway.
  1 +The file to generate given testlib.nim would be `Testlib.st`. This is the class
  2 +old "chunk format" in which the content is separated by `!` marks, in a rather
  3 +oddish way, but anyway.
2 4  
3 5 Let's go through it:
4 6  
5 7 ```
6 8 'From Nim on 18 February 2015 at 11:15:50 pm'!
7 9 ```
8   -Above first chunk is just a String literal with a timestamp, that chunk can be omitted.
  10 +Above first chunk is just a String literal with a timestamp, that chunk can
  11 +be omitted.
9 12  
10 13 ```
11 14 ExternalLibrary subclass: #Testlib
... ... @@ -14,9 +17,12 @@ ExternalLibrary subclass: #Testlib
14 17 poolDictionaries: ''
15 18 category: 'Nim'!
16 19 ```
17   -Then follows the class declaration. Obviously we need to make sure it says `Testlib` and the category will equal the package in Squeak that this class will end up in. For now, let's hard code it to `Nim`.
  20 +Then follows the class declaration. Obviously we need to make sure it says
  21 +`Testlib` and the category will equal the package in Squeak that this class
  22 +will end up in. For now, let's hard code it to `Nim`.
18 23  
19   -Then we can see that there is a "whitespace only" chunk, since the next part starts with `!`.
  24 +Then we can see that there is a "whitespace only" chunk, since the next part
  25 +starts with `!`.
20 26  
21 27 ```
22 28 !Testlib class methodsFor: 'primitives' stamp: 'gk 11/2/2014 13:05'!
... ...
tests/test1.nim 0 → 100644
  1 +
  2 +import squeaknim
  3 +
  4 +type
  5 + MyFloat = float32
  6 + Vector3 = object
  7 + x, y, z: MyFloat
  8 +
  9 +setModulename "urhonimo"
  10 +
  11 +wrapObject("Vector3", Vector3)
  12 +
  13 +writeExternalLibrary()
  14 +
  15 +proc foo(a, b: Vector3; c: openArray[int]): cstring {.exportSt: "bar".} =
  16 + result = "some string here"
  17 +
  18 +writeSmallTalkCode("test1.st")
... ...