Skip to content

Commit a79f1bd

Browse files
author
svacas-sfdc
committed
W-20884161: Add native DataWeave shared library with FFI bindings
refactor python lib
1 parent e535aa1 commit a79f1bd

18 files changed

Lines changed: 1817 additions & 8 deletions

File tree

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,6 @@ out/
1919
.DS_Store
2020

2121
# GraalVM
22-
.graalvm
22+
.graalvm
23+
24+
grimoires/

build.gradle

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
buildscript {
2+
repositories {
3+
gradlePluginPortal()
4+
mavenCentral()
5+
}
6+
dependencies {
7+
classpath "org.graalvm.buildtools.native:org.graalvm.buildtools.native.gradle.plugin:0.11.2"
8+
}
9+
}
10+
111
plugins {
212
id "scala"
313
id "maven-publish"
@@ -11,6 +21,8 @@ subprojects {
1121
apply plugin: 'maven-publish'
1222
apply plugin: 'scala'
1323

24+
apply plugin: 'org.graalvm.buildtools.native'
25+
1426
group = 'org.mule.weave.native'
1527
version = nativeVersion
1628

gradle.properties

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
weaveVersion=2.11.0-20251023
2-
weaveTestSuiteVersion=2.11.0-20251023
1+
weaveVersion=2.12.0-SNAPSHOT
2+
weaveTestSuiteVersion=2.12.0-SNAPSHOT
33
nativeVersion=100.100.100
44
scalaVersion=2.12.18
5-
ioVersion=2.11.0-SNAPSHOT
5+
ioVersion=2.12.0-SNAPSHOT
66
graalvmVersion=24.0.2
7-
weaveSuiteVersion=2.11.0-20251023
7+
weaveSuiteVersion=2.12.0-SNAPSHOT
88
#Libaries
99
scalaTestVersion=3.2.15
1010
scalaTestPluginVersion=0.33

native-cli/build.gradle

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ plugins {
22

33
id "com.github.maiflai.scalatest" version "${scalaTestPluginVersion}"
44
id 'application'
5-
// Apply GraalVM Native Image plugin
6-
id 'org.graalvm.buildtools.native' version '0.11.2'
75
}
86

97
sourceSets {

native-lib/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
python/src/dataweave/native/
2+
python/dist/

native-lib/README.md

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
# native-lib
2+
3+
## Overview
4+
5+
`native-lib` builds a **GraalVM native shared library** that embeds the MuleSoft **DataWeave runtime** and exposes a small C-compatible API.
6+
7+
The main purpose is to allow non-JVM consumers (most notably the Python package in `native-lib/python`) to execute DataWeave scripts **without running a JVM**, while still using the official DataWeave runtime.
8+
9+
## Architecture (GraalVM + FFI)
10+
11+
```
12+
┌─────────────────────────────────────────────┐
13+
│ Python Process │
14+
│ │
15+
│ ┌────────────────────────────────────────┐ │
16+
│ │ Application Script │ │
17+
│ │ - Python: ctypes │ │
18+
│ └──────────────┬─────────────────────────┘ │
19+
│ │ │
20+
│ │ FFI Call │
21+
│ ▼ │
22+
│ ┌────────────────────────────────────────┐ │
23+
│ │ Native Shared Library (dwlib) │ │
24+
│ │ ┌──────────────────────────────────┐ │ │
25+
│ │ │ GraalVM Isolate │ │ │
26+
│ │ │ - NativeLib.run_script() │ │ │
27+
│ │ │ - DataWeave script execution │ │ │
28+
│ │ └──────────────────────────────────┘ │ │
29+
│ └────────────────────────────────────────┘ │
30+
└─────────────────────────────────────────────┘
31+
```
32+
33+
## Building with Gradle
34+
35+
### Prerequisites
36+
37+
- A GraalVM distribution installed that includes `native-image`.
38+
- Enough memory for native-image (this build config uses `-J-Xmx6G`).
39+
40+
### Build the shared library
41+
42+
From the repository root:
43+
44+
```bash
45+
./gradlew :native-lib:nativeCompile
46+
```
47+
48+
The shared library is produced under:
49+
50+
- `native-lib/build/native/nativeCompile/`
51+
52+
and is named:
53+
54+
- macOS: `dwlib.dylib`
55+
- Linux: `dwlib.so`
56+
- Windows: `dwlib.dll`
57+
58+
### Stage the library into the Python package (dev workflow)
59+
60+
```bash
61+
./gradlew :native-lib:stagePythonNativeLib
62+
```
63+
64+
This copies `dwlib.*` into:
65+
66+
- `native-lib/python/src/dataweave/native/`
67+
68+
### Build a Python wheel (bundles the native library)
69+
70+
```bash
71+
./gradlew :native-lib:buildPythonWheel
72+
```
73+
74+
The wheel will be created in:
75+
76+
- `native-lib/python/dist/`
77+
78+
## Installing for use in a Python project
79+
80+
### Option A: Install the produced wheel (recommended)
81+
82+
After `:native-lib:buildPythonWheel`:
83+
84+
```bash
85+
python3 -m pip install native-lib/python/dist/dataweave_native-0.0.1-*.whl
86+
```
87+
88+
This wheel includes the `dwlib.*` shared library inside the Python package.
89+
90+
### Option B: Editable install for development
91+
92+
1. Stage the native library:
93+
94+
```bash
95+
./gradlew :native-lib:stagePythonNativeLib
96+
```
97+
98+
2. Install the Python package in editable mode:
99+
100+
```bash
101+
python3 -m pip install -e native-lib/python
102+
```
103+
104+
### Option C: Use an externally-built library via an environment variable
105+
106+
If you want to point Python at a specific built artifact, set:
107+
108+
- `DATAWEAVE_NATIVE_LIB=/absolute/path/to/dwlib.(dylib|so|dll)`
109+
110+
The Python module will also try a few fallbacks (including the wheel-bundled location).
111+
112+
## Using the library (Python examples)
113+
114+
All examples below assume:
115+
116+
```python
117+
import dataweave
118+
```
119+
120+
### 1) Simple script
121+
122+
```python
123+
result = dataweave.run_script("2 + 2")
124+
assert result.success is True
125+
print(result.get_string()) # "4"
126+
```
127+
128+
### 2) Script with inputs (no explicit `mimeType`)
129+
130+
Inputs can be plain Python values. The wrapper auto-encodes them as JSON or text.
131+
132+
```python
133+
result = dataweave.run_script(
134+
"num1 + num2",
135+
{"num1": 25, "num2": 17},
136+
)
137+
print(result.get_string()) # "42"
138+
```
139+
140+
### 3) Script with inputs (explicit `mimeType`, `charset`, `properties`)
141+
142+
Use an explicit input dict when you need full control over how DataWeave interprets bytes.
143+
144+
```python
145+
script = "payload.person"
146+
xml_bytes = b"<?xml version=\"1.0\" encoding=\"UTF-16\"?><person><name>Billy</name><age>31</age></person>".decode("utf-8").encode("utf-16")
147+
148+
result = dataweave.run_script(
149+
script,
150+
{
151+
"payload": {
152+
"content": xml_bytes,
153+
"mimeType": "application/xml",
154+
"charset": "UTF-16",
155+
"properties": {
156+
"nullValueOn": "empty",
157+
"maxAttributeSize": 256
158+
},
159+
}
160+
},
161+
)
162+
163+
if result.success:
164+
print(result.get_string())
165+
else:
166+
print(result.error)
167+
```
168+
169+
You can also use `InputValue` for the same purpose:
170+
171+
```python
172+
input_value = dataweave.InputValue(
173+
content="1234567",
174+
mimeType="application/csv",
175+
properties={"header": False, "separator": "4"},
176+
)
177+
178+
result = dataweave.run_script("in0.column_1[0]", {"in0": input_value})
179+
print(result.get_string()) # '"567"'
180+
```
181+
182+
### 4) Reusing a DataWeave context to run multiple scripts quicker
183+
184+
Creating an isolate/runtime has overhead. For repeated executions, reuse a single `DataWeave` instance:
185+
186+
```python
187+
with dataweave.DataWeave() as dw:
188+
r1 = dw.run("2 + 2")
189+
r2 = dw.run("x + y", {"x": 10, "y": 32})
190+
191+
print(r1.get_string()) # "4"
192+
print(r2.get_string()) # "42"
193+
```
194+
195+
### 5) Error handling
196+
197+
There are two common classes of errors:
198+
199+
- The native library cannot be located/loaded.
200+
- Script compilation/execution fails (reported as an unsuccessful `ExecutionResult`).
201+
202+
```python
203+
try:
204+
result = dataweave.run_script("invalid syntax here")
205+
206+
if not result.success:
207+
raise dataweave.DataWeaveError(result.error or "Unknown DataWeave error")
208+
209+
print(result.get_string())
210+
211+
except dataweave.DataWeaveLibraryNotFoundError as e:
212+
# Build it (and/or install a wheel) first.
213+
# Example build command (from repo root): ./gradlew :native-lib:nativeCompile
214+
raise
215+
216+
except dataweave.DataWeaveError:
217+
raise
218+
219+
finally:
220+
# Optional: if you used the global API and want to force cleanup
221+
dataweave.cleanup()
222+
```

native-lib/build.gradle

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
dependencies {
2+
api group: 'org.mule.weave', name: 'runtime', version: weaveVersion
3+
api group: 'org.mule.weave', name: 'core-modules', version: weaveVersion
4+
5+
implementation group: 'org.mule.weave', name: 'parser', version: weaveVersion
6+
implementation group: 'org.mule.weave', name: 'wlang', version: weaveVersion
7+
compileOnly group: 'org.graalvm.sdk', name: 'graal-sdk', version: graalvmVersion
8+
compileOnly group: 'org.graalvm.nativeimage', name: 'svm', version: graalvmVersion
9+
10+
implementation "org.scala-lang:scala-library:${scalaVersion}"
11+
12+
testImplementation platform('org.junit:junit-bom:5.10.0')
13+
testImplementation 'org.junit.jupiter:junit-jupiter'
14+
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
15+
}
16+
17+
test {
18+
useJUnitPlatform()
19+
}
20+
21+
tasks.matching { it.name == 'nativeCompileClasspathJar' }.configureEach { t ->
22+
t.exclude('META-INF/services/org.mule.weave.v2.module.DataFormat')
23+
t.from("${projectDir}/src/main/resources/META-INF/services/org.mule.weave.v2.module.DataFormat") {
24+
into('META-INF/services')
25+
rename { 'org.mule.weave.v2.module.DataFormat' }
26+
}
27+
}
28+
29+
// Configure GraalVM native-image to build a shared library
30+
graalvmNative {
31+
// toolchainDetection = true
32+
binaries {
33+
main {
34+
sharedLibrary = true
35+
debug = true
36+
verbose = true
37+
fallback = false
38+
//agent = false
39+
useFatJar = true
40+
//buildArgs.add('-Ob') // quick build mode to speed up builds during development
41+
buildArgs.add('--no-fallback')
42+
buildArgs.add('-H:Name=dwlib')
43+
buildArgs.add('--verbose')
44+
buildArgs.add("--report-unsupported-elements-at-runtime")
45+
buildArgs.add("-J-Xmx6G")
46+
47+
buildArgs.add("-H:+ReportExceptionStackTraces")
48+
buildArgs.add("-H:+UnlockExperimentalVMOptions")
49+
buildArgs.add("--initialize-at-build-time=sun.instrument.InstrumentationImpl")
50+
buildArgs.add("-H:DeadlockWatchdogInterval=1000")
51+
buildArgs.add("-H:CompilationExpirationPeriod=0")
52+
buildArgs.add("-H:+AddAllCharsets")
53+
buildArgs.add("-H:+IncludeAllLocales")
54+
// Pass project directory as system property for header path resolution
55+
buildArgs.add("-Dproject.root=${projectDir}")
56+
}
57+
}
58+
}
59+
60+
def pythonExe = (project.findProperty('pythonExe') ?: 'python3') as String
61+
62+
tasks.register('stagePythonNativeLib', Copy) {
63+
dependsOn tasks.named('nativeCompile')
64+
from("${buildDir}/native/nativeCompile") {
65+
include('dwlib.*')
66+
}
67+
into("${projectDir}/python/src/dataweave/native")
68+
}
69+
70+
tasks.register('buildPythonWheel', Exec) {
71+
dependsOn tasks.named('stagePythonNativeLib')
72+
workingDir("${projectDir}/python")
73+
outputs.dir("${projectDir}/python/dist")
74+
doFirst {
75+
file("${projectDir}/python/dist").mkdirs()
76+
}
77+
commandLine(pythonExe, '-m', 'pip', 'wheel', '--no-deps', '-w', 'dist', '.')
78+
}
79+
80+
tasks.register('pythonTest', Exec) {
81+
if (project.findProperty('skipPythonTests')?.toString()?.toBoolean() == true) {
82+
enabled = false
83+
}
84+
85+
dependsOn tasks.named('stagePythonNativeLib')
86+
workingDir("${projectDir}/python")
87+
commandLine(pythonExe, 'tests/test_dataweave_module.py')
88+
}
89+
90+
tasks.named('test') {
91+
dependsOn tasks.named('pythonTest')
92+
}
93+
94+
tasks.named('clean') {
95+
delete("${projectDir}/python/dist")
96+
delete("${projectDir}/python/src/dataweave/native")
97+
}

0 commit comments

Comments
 (0)