Commit f149dc12fcc4a5ab3a6f6715736ed3139cdfcc10
1 parent
24d7cba1
first somewhat working version of SqueakNim
Showing
4 changed files
with
212 additions
and
16 deletions
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") | ... | ... |